The Memory API exposes the read surfaces over an agent’s episodic and markdown memory: a health snapshot that scores how usable the memory is, a hybrid recall search that fuses two retrieval engines, and the version audit trail for canonical memory files. The mutating restore endpoint lives on the Admin reference because it requires OWNER/ADMIN.
Every endpoint requires authentication and workspace context. workspace_id is always pulled from the session — never accepted as a query parameter or body field — and crew_id, when set, is validated against the caller’s workspace (cross-tenant lookups return 404).
Endpoints
| Method | Endpoint | Purpose |
|---|
| GET | /api/v1/memory/health | Memory health snapshot (five metrics + composite) |
| POST | /api/v1/memory/search/hybrid | Fused FTS5 + episodic memory recall |
| GET | /api/v1/memory/versions | Audit chain for one canonical memory path |
| GET | /api/v1/memory/versions/{sha} | Raw blob bytes for one historical version |
Health
Read-only snapshot for the memory health dashboard. Backed by internal/consolidate.ComputeHealth. The five aggregate queries it runs are cheap, so the handler recomputes on every request rather than reading the persisted memory_health_snapshots table — those snapshots exist for time-series plots, not real-time reads.
See Episodic memory — health scoring for the formula and operator interpretation.
Get health snapshot
GET /api/v1/memory/health
Query parameters:
| Param | Type | Description |
|---|
crew_id | string | Optional. Crew ID (not slug) to scope the score to a single crew. Omit for workspace-aggregate. The ID is validated against the caller’s workspace via crewBelongsToWorkspace; a foreign ID returns 404. |
Auth: Every workspace member can read. The response contains only counts and ratios, no raw entry content.
Response: 200 OK
{
"workspace_id": "ws_123",
"crew_id": "",
"computed_at": "2026-04-30T11:42:18.018Z",
"overall": 77.4,
"metrics": {
"freshness": 83.1,
"coverage": 62.0,
"coherence": 80.2,
"efficiency": 77.4,
"reachability": 78.0
},
"details": {
"freshness_recent_24h": 12,
"freshness_baseline_daily": 9.4,
"coverage_distinct_types": 5,
"coherence_relations": 1284,
"coherence_embeddings": 1390,
"efficiency_archived": 640,
"efficiency_live": 1390,
"reachability_connected": 1084
}
}
| Field | Type | Description |
|---|
workspace_id | string | Echo of the session’s workspace. |
crew_id | string | Echo of the request’s crew_id (empty for workspace-aggregate). |
computed_at | string (RFC3339) | When this response was computed. The handler always recomputes, so this is now. |
overall | number | Weighted composite, 0–100. Formula: 0.25·freshness + 0.25·coverage + 0.20·coherence + 0.15·efficiency + 0.15·reachability. |
metrics.* | number | Each metric, 0–100. See the guide for what each one measures. |
details | object | Free-form supplementary data — the per-metric inputs each score was derived from (freshness_recent_24h, coverage_distinct_types, coherence_relations/coherence_embeddings, efficiency_archived/efficiency_live, reachability_connected, …). Schema is not stable across versions; consume defensively. |
There is no separate band field — clients categorise locally:
| Band | Range |
|---|
| Red | overall < 50 |
| Yellow | 50 ≤ overall < 75 |
| Green | overall ≥ 75 |
The CLI uses these exact thresholds (internal/cli colour helpers) and the FE follows.
Errors:
| Status | Condition |
|---|
| 401 | No workspace context in session. |
| 404 | crew_id set and not in caller’s workspace. |
| 500 | Compute failure — propagated from ComputeHealth. |
Hybrid search
POST /api/v1/memory/search/hybrid
Single-shot memory recall that fuses two retrieval engines and merges
their results with Reciprocal Rank Fusion (RRF, k = 60):
- FTS5 / BM25 — the workspace-tier full-text index over markdown memory.
- Episodic recall — dense-vector + BM25 recall over the crew journal.
The handler degrades gracefully: if only one engine is wired (no
embedder, or no FTS engine for the workspace), it returns that engine’s
results alone; if neither has matches it returns 200 with an empty
hits array and count: 0 — the same shape the sidecar /memory/search
uses.
Auth: required + workspace context (MEMBER+). Every query is anchored
on the caller’s session workspace, so a foreign workspace_id cannot be
smuggled through the body.
Request body:
{
"query": "deploy script flakiness",
"limit": 10,
"scope": "crew_shared",
"crew_id": "crew_123"
}
| Field | Type | Default | Description |
|---|
query | string | — | Required. The search text. 400 when empty. |
limit | integer | 10 | Max hits to return. Non-positive values fall back to the default; capped at 50 server-side. |
scope | string | "" | One of "" (no scope filter, MEMBER+ implied), "own" (caller’s own memory only), or "crew_shared" (crew-shared memory). Any other value is 400. |
crew_id | string | — | Optional. Binds crew_shared results to a single crew. |
Response: 200 OK
{
"query": "deploy script flakiness",
"count": 2,
"hits": [
{
"source": "fts",
"score": 0.0163,
"fts": { "...": "FTS5 chunk hit" }
},
{
"source": "episodic",
"score": 0.0159,
"episodic": { "...": "journal entry hit" }
}
]
}
The unused half of each hit is omitted, not emitted as null
(fts / episodic both carry omitempty): an FTS-sourced hit has no
episodic key and vice-versa.
| Field | Type | Description |
|---|
query | string | Echo of the request’s query. |
count | integer | len(hits). |
hits[] | array | Fused, RRF-ranked results. |
hits[].source | string | "fts" or "episodic" — which engine produced the hit. |
hits[].score | number | The RRF score used for ranking (higher = better). |
hits[].fts | object? | The FTS5 chunk payload; omitted on episodic-sourced hits. |
hits[].episodic | object? | The episodic journal-entry payload; omitted on FTS-sourced hits. |
| Status | Condition |
|---|
400 | Malformed JSON, empty query, or an invalid scope. |
401 | No workspace context in the session. |
500 | Underlying memory.HybridSearch failed. |
Memory versions audit trail
The HTTP mirror of crewship memory log/show. Workspace is anchored from the session — query strings never carry workspace_id, so a cross-workspace probe can’t smuggle a foreign id through. Restore (POST /api/v1/memory/versions/{sha}/restore) is documented separately on the Admin reference because it requires OWNER/ADMIN and mutates canonical state; the two read endpoints below are MEMBER+.
GET /api/v1/memory/versions
Returns the audit chain for one canonical memory path, newest-first.
Auth: required + workspace context.
Query parameters:
| Param | Type | Default | Description |
|---|
path | string | — | Required. Canonical memory path (e.g. agent:martin/AGENT.md). 400 when omitted. |
limit | integer | 20 | Max rows to return. Positive values are accepted and clamped to a hard ceiling of 1000; non-positive or non-numeric values fall back to the default. |
Response: 200 OK
{
"path": "agent:martin/AGENT.md",
"count": 2,
"entries": [
{
"id": "mv_01HZA...",
"path": "agent:martin/AGENT.md",
"tier": "agent",
"sha256": "abc...",
"bytes": 1284,
"written_at": "2026-05-18T08:30:00Z",
"written_by": "audit-watcher",
"parent_sha": "def...",
"payload_ref": "/var/lib/crewship/memory/versions/ab/abc..."
},
{
"id": "mv_01HZ9...",
"path": "agent:martin/AGENT.md",
"tier": "agent",
"sha256": "def...",
"bytes": 1180,
"written_at": "2026-05-17T22:11:48Z",
"written_by": "audit-watcher",
"payload_ref": "/var/lib/crewship/memory/versions/de/def..."
}
]
}
| Field | Type | Description |
|---|
path | string | Echo of the request’s path. |
count | integer | len(entries) — handy when paging is bounded by limit. |
entries[] | array | Newest-first list of memory_versions rows for this path in the caller’s workspace. |
entries[].tier | string | agent, crew, workspace, pins, or learned. |
entries[].parent_sha | string? | sha256 of the prior version in the chain. Omitted (not null) on the root version — the field carries omitempty and the column is COALESCE’d to "". |
entries[].payload_ref | string | On-disk path to the content-addressed blob. |
| Status | Condition |
|---|
400 | Missing path query parameter. |
401 | No workspace context in the session. |
500 | Underlying memory.LogVersions query failed. |
GET /api/v1/memory/versions/{sha}
Returns the raw blob bytes for one historical version. Pipe-friendly: the body is the historical content, metadata travels in headers so a CLI pipe to a file produces the exact bytes that were written.
Auth: required + workspace context.
Path parameters:
| Param | Description |
|---|
sha | Content sha256 from memory_versions.sha256. |
Query parameters:
| Param | Type | Description |
|---|
path | string | Required. The canonical memory path the sha was written under. The same sha can be paired with different paths if two paths happen to converge on identical content. |
Response: 200 OK — raw bytes, Content-Type: application/octet-stream.
| Header | Description |
|---|
X-Memory-Version-Sha | Echo of the path’s sha. |
X-Memory-Version-Bytes | Body length (matches memory_versions.bytes). |
| Status | Condition |
|---|
400 | Missing sha path segment or missing path query parameter. |
401 | No workspace context. |
404 | No memory_versions row matches (workspace_id, path, sha) — cross-workspace lookups masked as 404 to avoid leaking row existence. |
500 | Read failure from the underlying blob store. |
The restore endpoint (POST /api/v1/memory/versions/{sha}/restore) is OWNER/ADMIN-only and applies extra server-side path confinement to refuse canonical targets outside the configured memory root. See the Admin reference for the full mutation surface.
Tenancy
workspace_id is always pulled from the session — never accepted as a query parameter or body field.
crew_id, when set, is validated via crewBelongsToWorkspace (same helper as the Paymaster API). Cross-tenant lookups return 404.