REST API

The Rust/Axum pensyve-mcp-gateway exposes the Pensyve memory runtime over HTTP and serves MCP on the same port.

cargo build --release -p pensyve-mcp-gateway
./target/release/pensyve-mcp-gateway

Base URL: http://localhost:3000


Authentication

Auth is opt-in. Set PENSYVE_API_KEYS to a comma-separated list of keys. When set, every request must include:

Authorization: Bearer your-api-key

When PENSYVE_API_KEYS is unset, all endpoints are open.


Result Limits

Recall and inspect accept a limit in the JSON request body. They do not currently return a cursor or continuation token, so raise limit when you need a larger result set.


Endpoints

GET /v1/health

Health check.

Response:

{ "status": "ok", "version": "3.1.0" }
curl http://localhost:3000/v1/health

POST /v1/entities

Create or get an entity.

Request body:

FieldTypeDefaultDescription
namestringrequiredEntity name
kindstring"user""agent", "user", "team", or "tool"

Response: 201

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "alice",
  "kind": "user"
}
curl -X POST http://localhost:3000/v1/entities \
  -H "Content-Type: application/json" \
  -d '{"name": "alice", "kind": "user"}'

POST /v1/remember

Store a semantic memory about an entity.

Request body:

FieldTypeDefaultDescription
entitystringrequiredEntity name
factstringrequiredThe fact to store
confidencenumber0.8Confidence in [0, 1]

Response: 201

{
  "id": "a1b2c3d4-...",
  "content": "Alice prefers dark mode",
  "memory_type": "semantic",
  "confidence": 0.8,
  "stability": 1.0,
  "extraction_tier": 1
}
curl -X POST http://localhost:3000/v1/remember \
  -H "Content-Type: application/json" \
  -d '{"entity": "alice", "fact": "Alice prefers dark mode"}'

POST /v1/recall

Search memories. Fuses vector, BM25, graph, recency, and other signals.

Request body:

FieldTypeDefaultDescription
querystringrequiredSearch query
entitystring | nullnullFilter to a specific entity
limitinteger5Max results per page
typesstring[] | nullnullFilter by memory type: "episodic", "semantic", "procedural"

Response: 200

{
  "memories": [
    {
      "id": "a1b2c3d4-...",
      "content": "Alice prefers dark mode",
      "memory_type": "semantic",
      "confidence": 0.8,
      "stability": 1.0,
      "score": 0.87
    }
  ],
  "contradictions": []
}

The gateway compares returned semantic memories for conflicting facts and includes any findings in contradictions. Each entry is {"description": "..."}.

curl -X POST http://localhost:3000/v1/recall \
  -H "Content-Type: application/json" \
  -d '{"query": "dark mode preference", "entity": "alice", "limit": 10}'

POST /v1/recall_grouped

Search memories and cluster them by source session in one round trip. Same RRF fusion pipeline as /v1/recall, post-processed by the core engine to group results by episode_id. The canonical entry point for "memory as input to an LLM reader" workflows.

Internal benchmarking on LongMemEval_S confirmed that this layout produces materially better reader accuracy than flat recall — moving the session-clustering step into the engine eliminates a class of consumer-side reordering bugs and matches the prompt format the underlying retrieval pipeline is tuned for.

Request body:

FieldTypeDefaultDescription
querystringrequiredSearch query
limitinteger50Max memories to consider across all groups
order"chronological" | "relevance""chronological"Group ordering (oldest-first or highest-scoring-first)
max_groupsinteger | nullnullOptional cap on the number of returned groups

Response: 200

{
  "groups": [
    {
      "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "session_time": "2026-01-01T10:00:00+00:00",
      "group_score": 0.92,
      "memories": [
        {
          "id": "a1b2c3d4-...",
          "content": "user: I bought three books yesterday",
          "memory_type": "episodic",
          "confidence": 1.0,
          "stability": 0.8,
          "score": 0.92
        },
        {
          "id": "b2c3d4e5-...",
          "content": "assistant: Nice — any you'd recommend?",
          "memory_type": "episodic",
          "confidence": 1.0,
          "stability": 0.8,
          "score": 0.92
        }
      ]
    },
    {
      "session_id": null,
      "session_time": "2026-02-01T09:00:00+00:00",
      "group_score": 0.51,
      "memories": [
        {
          "id": "c3d4e5f6-...",
          "content": "Alice prefers hardcover",
          "memory_type": "semantic",
          "confidence": 0.9,
          "stability": 1.0,
          "score": 0.51
        }
      ]
    }
  ]
}

session_id is null for semantic and procedural memories that have no episode ancestor — they surface as singleton groups. The default chronological order matches the LongMemEval-validated layout: oldest session first, with within-group memories in conversation order.

Errors:

  • 400 Bad Request if order is not "chronological" or "relevance".
  • 500 Internal Server Error if the underlying recall pipeline fails.
curl -X POST http://localhost:3000/v1/recall_grouped \
  -H "Content-Type: application/json" \
  -d '{"query": "How many books did I buy?", "limit": 50, "order": "chronological"}'

POST /v1/episodes/start

Begin tracking an interaction episode.

Request body:

FieldTypeDefaultDescription
participantsstring[]requiredEntity names of the participants

Response: 200

{
  "episode_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

Episodes expire after 30 minutes of inactivity.

curl -X POST http://localhost:3000/v1/episodes/start \
  -H "Content-Type: application/json" \
  -d '{"participants": ["alice", "my-agent"]}'

POST /v1/episodes/{id}/message

Add a message to an active episode.

Path parameters:

ParamTypeDescription
idstringEpisode ID from /v1/episodes/start

Request body:

FieldTypeDefaultDescription
rolestringrequiredSpeaker role (e.g. "user", "assistant")
contentstringrequiredMessage text

Response: 200

{ "status": "ok" }

Error: 404 if the episode ID is not found or has expired.

curl -X POST http://localhost:3000/v1/episodes/f47ac10b-.../message \
  -H "Content-Type: application/json" \
  -d '{
    "role": "user",
    "content": "What is the status of project X?"
  }'

POST /v1/episodes/{id}/end

Close an episode and extract memories.

Path parameters:

ParamTypeDescription
idstringEpisode ID

Request body:

FieldTypeDefaultDescription
outcomestring | nullnull"success", "failure", or "partial"

Response: 200

{
  "memories_created": 3
}

Error: 404 if the episode ID is not found.

curl -X POST http://localhost:3000/v1/episodes/f47ac10b-.../end \
  -H "Content-Type: application/json" \
  -d '{"outcome": "success"}'

DELETE /v1/entities/{entity_name}

Delete all active memories for an entity. The gateway writes a pre-delete snapshot in the same operation and aborts the deletion if the snapshot cannot be created.

Path parameters:

ParamTypeDescription
entity_namestringEntity name

Percent-encode entity_name as one URI path segment before inserting it into the URL. In JavaScript, use encodeURIComponent(entityName) rather than encodeURI(entityName) so slashes, query markers, fragments, and spaces cannot change the request path.

Response: 200

{
  "forgotten_count": 12,
  "snapshot": {
    "snapshot_id": "8c3cb3d4-...",
    "format_version": 1,
    "captured_at": "2026-08-21T15:00:00Z",
    "owner_only": true,
    "memory_count": 12,
    "episodic_count": 4,
    "semantic_count": 8
  }
}

The snapshot field is omitted when no memories were deleted. It is an operator recovery reference; the public REST API does not currently expose a restore endpoint.

curl -X DELETE http://localhost:3000/v1/entities/alice

For an entity named Alice/Research?phase=1#notes, encode the full value as a single segment:

curl -X DELETE 'http://localhost:3000/v1/entities/Alice%2FResearch%3Fphase%3D1%23notes'

POST /v1/inspect

View all memories for an entity, grouped by type.

Request body:

FieldTypeDefaultDescription
entitystringrequiredEntity name
limitinteger50Max results per page

There is currently no cursor/token for paging past limit; raise limit to widen the page.

Response: 200

{
  "entity": "alice",
  "episodic": [
    {
      "id": "...",
      "content": "Asked about project X",
      "memory_type": "episodic",
      "confidence": 1.0,
      "stability": 0.95,
      "score": null
    }
  ],
  "semantic": [],
  "procedural": []
}
curl -X POST http://localhost:3000/v1/inspect \
  -H "Content-Type: application/json" \
  -d '{"entity": "alice"}'

GET /v1/stats

Memory statistics for the current namespace.

Response: 200

{
  "namespace": "default",
  "entities": 0,
  "episodic_memories": 42,
  "semantic_memories": 15,
  "procedural_memories": 3
}
curl http://localhost:3000/v1/stats

POST /v1/consolidate

Trigger background consolidation. Promotes repeated episodic memories to semantic, applies FSRS decay, and archives memories below threshold.

Request body: None.

Response: 200

{
  "promoted": 3,
  "decayed": 12,
  "archived": 1
}
curl -X POST http://localhost:3000/v1/consolidate

Error Responses

All errors return JSON:

{
  "error": "Episode f47ac10b-... not found"
}
StatusMeaning
400Invalid request body
401Missing or invalid Authorization: Bearer token
404Resource not found (episode, entity)
422Malformed or incomplete JSON request
500Internal server error

Environment Variables

VariableDefaultPurpose
HOST0.0.0.0Gateway bind address
PORT3000REST and MCP gateway port
PENSYVE_PATH~/.pensyve/gatewaySQLite database path
PENSYVE_NAMESPACE"default"Memory namespace
PENSYVE_API_KEYSunsetComma-separated API keys
PENSYVE_RATE_LIMIT300Requests per minute per API key
PENSYVE_EXTRACTOR_URLhttp://localhost:8888/v1Local OpenAI-compatible extraction endpoint
PENSYVE_EXTRACTOR_MODELqwen3.6-35b-a3bModel name sent to the extraction endpoint