In-Session Memory Capture (#520)
August 9, 2026 · View on GitHub
Perseus Vault can distill a session transcript or insight payload into durable memory entities the moment a problem is solved, instead of waiting for a scheduled harvest to notice. This is the capture pipeline: one code path exposed two ways —
- CLI:
perseus-vault capture(stdin or--file) - MCP:
perseus_vault_capture(aliasesperseus_vault_capture,perseus_vault_capture)
Off by default, local-first. Nothing captures automatically: no config flag turns this on in the background, and the default distiller makes zero network and zero LLM calls. Capture happens only when you (or a lifecycle hook you wrote) explicitly invoke the verb or the tool. All writes go to your local SQLite database.
What it does
- Splits the payload into candidate notes. Three shapes, auto-detected:
- JSONL — one note per record (uses its
content/text/insight/lesson/summary/messagefield, else the compact record); - Headed markdown — one note per
#-headed section; - Plain text — one note per blank-line-separated paragraph. Trivially short chatter ("ok", "done") is discarded — precision over recall.
- JSONL — one note per record (uses its
- Classifies each note by cheap local signals into an entity type:
root-cause(a failure plus its diagnosis),pitfall(a failure to avoid — markers aligned with theperseus_vault_check_failure_patterndeja-vu guard, #521, so captured pitfalls are findable by it),decision,pattern, ortakeaway. - Keys each note with a stable slug of its summary line, and
remembers it through the normal write path:
category="capture",source="capture", layerbuffer, moderate importance (0.6). Captured memories then live the standard lifecycle — decay, promotion, consolidation, recall — like any other entity.
Flood control (by design)
- Near-duplicate merging stays ON. A re-captured solved problem
(reworded, different headline) merges into the existing memory via the
trigram dedup instead of piling up siblings. There is deliberately no
skip_dedupfor captures. - Same headline → same key → in-place update, not a new row.
- Hard cap per invocation (20; callers can lower it with
max_entities, never raise it). Notes beyond the cap are dropped and counted in the result (dropped), never silently eaten. --dry-run/dry_run: truedistills and reports without writing.
CLI usage
# From a hook or pipeline: distill stdin, write to the default vault
some-agent --dump-session | perseus-vault capture
# From a file, scoped to a workspace, preview only
perseus-vault capture --file transcript.jsonl --workspace-hash ws-myproj --dry-run
# Optional LLM distillation (falls back to the rule-based path on ANY
# LLM failure or timeout — see PERSEUS_VAULT_LLM_TIMEOUT_SECS, #528)
perseus-vault capture --file notes.md --llm \
--llm-endpoint http://localhost:11434/api/generate --llm-model llama3
Output is a JSON report: captured / created / updated / merged /
dropped, the distiller used (rule_based or llm, plus llm_fallback
with the reason when the LLM path degraded), and a per-note breakdown
(key, type, summary, action). A failed capture exits non-zero AND
prints {"ok": false, ...} (#516 pattern).
MCP usage
{ "name": "perseus_vault_capture",
"arguments": { "text": "<transcript or insight payload>",
"workspace_hash": "ws-myproj", "dry_run": false } }
Same pipeline, same report, same flood control. llm: true uses the
server's configured --llm-endpoint and degrades identically.
Wiring it to session lifecycle hooks
Capture is the on_insight / SessionEnd stage of the session lifecycle contract (see the hook contract doc from #523/#540 — SessionStart recalls, on_insight captures, SessionStop consolidates):
- on_insight (mid-session): the moment something durable happens, pipe
it in —
echo "<the insight>" | perseus-vault capture— or callperseus_vault_capturefrom the agent. Near-dup merging makes repeated captures of the same insight harmless. - SessionEnd: capture first, then run the hygiene pass — the captured
notes land in the
bufferlayer andmaintainimmediately gets a chance to merge/promote them:
{
"hooks": {
"SessionEnd": [
{ "matcher": "*",
"hooks": [
{ "type": "command",
"command": "cat \"$CLAUDE_TRANSCRIPT_PATH\" | perseus-vault capture --dry-run",
"timeout": 60 },
{ "type": "command",
"command": "perseus-vault maintain",
"timeout": 120 }
] }
]
}
}
(Drop --dry-run once you've previewed what your transcripts distill
into. Order matters: capture, then maintain — end-of-session
semantics are "persist what was learned, then groom".)
The LLM path is optional, the rule-based path is the floor
The default distiller is pure, deterministic Rust — the same air-gapped
bar as perseus_vault_extract (#234). With llm: true / --llm, the configured
endpoint is asked to distill instead (strict-JSON contract; unknown entity
types degrade to takeaway — model output is untrusted). On any LLM
failure — endpoint not configured, transport error, timeout
(PERSEUS_VAULT_LLM_TIMEOUT_SECS, default 30s), or unparseable output — the
pipeline falls back to the rule-based distiller and says so in the
report's llm_fallback field. A capture invocation never comes back
empty-handed because a model was slow, down, or chatty.