[PROPOSAL] Expose compact/session lifecycle hooks for external memory layers
Problem
Five open issues request persistent memory (#14227, #32627, #34192, #34556, #46138). The community is already building solutions — 3-tier markdown architectures, knowledge graphs, structured memory layers. Each re-invents transcript access and compact interception because Claude Code doesn't expose the lifecycle events external memory needs.
The platform doesn't need to build persistent memory. It needs to expose the seams so the community can build it composably.
Proposed hooks
1. PreCompact — fires before context compaction
// settings.json
{
"hooks": {
"PreCompact": [{
"type": "command",
"command": "bella save --auto"
}]
}
}
What it enables: External memory layers save structured state before compaction flattens the context. Currently, if compaction happens between user messages, unfiled knowledge is lost (as documented in #34556 — 59 compactions, each a potential knowledge-loss event).
What the hook receives: Current session ID, token count, transcript path.
What the hook can return: Optional additionalContext to inject into the post-compaction context (e.g., a structured summary from the memory layer, replacing the lossy default compaction).
2. PostCompact — fires after compaction completes
What it enables: Memory layers can inject recalled context into the fresh post-compaction window. Different from PreCompact — this is the retrieval side, not the save side.
What the hook receives: Session ID, new token budget available, compaction summary.
3. SessionEnd — fires when a session terminates
What it enables: Final save opportunity. Currently, if the user closes the terminal or the process exits, no save happens.
What the hook receives: Session ID, transcript path, exit reason.
4. SessionStart — fires on session initialization (before CLAUDE.md)
What it enables: Memory layers inject recalled context before the first user message. Currently achievable via UserPromptSubmit hooks but semantically wrong — session start is the right lifecycle event.
What the hook receives: Session ID, project root, cwd.
Prior art
- Claude Code's existing
UserPromptSubmithook is used by multiple memory projects as a workaround forSessionStart, but it fires on every message, not just session init. - The
PreCompacthook is specifically requested in #31845. - Bella is an open-source belief hypergraph memory layer that would immediately use all four hooks. Currently works around the missing lifecycle by intercepting transcripts post-hoc.
- The theoretical basis is Recursive Emergence — a framework for how beliefs should emerge, reinforce, and decay in bounded-memory systems.
What this does NOT propose
- No opinion on what memory should look like (flat files, knowledge graphs, hypergraphs — that's the community's job)
- No changes to /compact behavior itself
- No new built-in memory system
This is purely about exposing lifecycle seams so the five existing community efforts can compose with Claude Code instead of fighting it.
41 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
We've been building Bella — the belief hypergraph memory layer for Claude Code (and any AI agents), with visible extension of time horizon (8× or longer) — but currently work around every missing hook listed here. Sharing concrete implementation experience on each:
PreCompact — the biggest gap, no workaround exists
This is the one we can't work around at all. When auto-compact fires, Bella has no chance to ingest the about-to-be-flattened context. Everything between the last
bellamem saveand the compaction event is silently lost.Our current mitigation is a 5-minute cron (
*/5 * * * * bellamem save) that ingests incrementally — but that's a probabilistic hedge, not a guarantee. Any claims made in the last 0–5 minutes before compaction are gone.What the
additionalContextreturn should support: Not just a narrative summary — the whole point is that narrative compaction loses the specific decisions, corrections, and causal chains an agent needs. Bella'sexpand(focus, budget)already produces a token-budgeted, mass-ranked belief pack with disputes and causes preserved. On our bench (mature 1834-belief forest), this scores 92% LLM-judge vs 8% for a narrative compact summary on the same material. The hook should let the memory layer replace the default compaction output, not just append to it.PostCompact — the retrieval side
Bella's
expand()is designed for exactly this: given a focus query and a token budget, return the highest-value beliefs from the persistent graph. The compaction summary itself would be the natural focus query — "what was this session about?" → retrieve the graph's structured version of the same content.Currently we achieve this manually via
/bellamem resumeafter/clear. A PostCompact hook would make it automatic and invisible.SessionEnd — currently a cron workaround
Same 5-minute cron. If the user closes the terminal at minute 3, we lose 3 minutes of claims. SessionEnd with access to the transcript path would let us do a final
bellamem saveand guarantee nothing is lost.One implementation detail: the hook should fire while the transcript
.jsonlis still complete and readable. If Claude Code truncates or archives the file before the hook fires, the final ingest will miss the tail.SessionStart — currently a UserPromptSubmit workaround
We use the
bellamem.mdslash command via UserPromptSubmit to inject the resume pack. It works, but it fires on every user message, so the command has to be guarded to only trigger on the first invocation. SessionStart is the semantically correct lifecycle event.Existence proof: PreToolUse already works this way
Bella ships a
bellamem-guardPreToolUse hook that injects an advisory pack (invariants + disputes + causes) before every Edit/Write/MultiEdit call. Itexit 2s when the agent re-suggests a rejected approach. This proves the pattern — lifecycle hooks that let external memory layers inject structured context at decision boundaries. PreCompact/PostCompact are the same pattern applied to the compaction boundary instead of the tool-call boundary.What we'd build on day one
If these four hooks shipped, our cron workaround and slash-command workaround both become unnecessary. The save→clear→resume loop (
/bellamem save→/clear→/bellamem resume) collapses into automatic lifecycle management. And the PreCompactadditionalContextpath would let us ship graph-backed compaction — the single biggest remaining limitation in our README.This isn't a duplicate of any single issue — it's the consolidation that the three referenced issues need.
This proposal unifies all four hooks (PreCompact, PostCompact, SessionStart, SessionEnd) into a single coherent lifecycle model, backed by a working implementation that currently works around every missing hook and would use all four on day one.
The individual issues each request a piece. This one proposes the seam — the minimal complete set of lifecycle events that external memory layers need to compose with Claude Code instead of fighting it. Closing this as a duplicate of any one of the three loses the unification.
Happy to cross-reference from the other issues if that helps consolidate attention.
+1 from production experience. NEXO Brain has been using PreCompact, PostCompact, Stop, and SessionStart hooks daily for months — they are the backbone of our cross-session memory system.
Concrete data point: before PreCompact existed as a hook, every compaction was a hard reset. After wiring it to trigger automatic diary writes + checkpoint saves, session continuity went from "hope the model remembers" to deterministic. PostCompact reads the checkpoint back and the session resumes warm. This single hook (PreCompact) made external persistent memory viable.
Agreeing with @immartian that this is not a duplicate — the referenced issues each cover a subset. This proposal consolidates them into the minimal, composable surface area that external memory layers actually need. Two independent projects (Bella and NEXO Brain) arriving at the same hook requirements independently is a strong signal.
The key insight in this proposal is right: the platform does not need to build memory — it needs to expose the seams. Hooks are the correct abstraction level.
Strong proposal — the four hooks you describe are exactly what external memory needs. Wanted to share that Cozempic's plugin already wires SessionStart, PreCompact, PostCompact, and Stop hooks today via
hooks.json, so this pattern is proven in production.The guard daemon uses SessionStart to pick up the active session ID, PreCompact/PostCompact for checkpointing state, and Stop for final cleanup. We've also been building a behavioral digest layer on top of these hooks — it extracts correction patterns from the session delta at PreCompact time and re-injects them post-compaction so learned rules survive the context reset.
If you want to experiment with this hook surface today:
The hooks are defined in
plugin/hooks.jsonand the source is fully readable. Happy to compare notes on theadditionalContextreturn path — that's the piece we'd love to see standardized upstream.https://github.com/Ruya-AI/cozempic
The
additionalContextreturn needs two design decisions settled before implementation.Replace vs. append. immartian's 92% result comes from replacing the native compaction, not supplementing it — but the proposed signature doesn't make that explicit. A mode field (
{ "mode": "replace" | "append", "content": "..." }) handles both and makes the default (no return → native compaction) unambiguous.Fallback on failure. If the memory layer crashes or returns malformed content, falling back to native compaction silently is the right call — but there's a third state: the layer succeeds but the output fails a faithfulness check (key names dropped, numeric values gone, hallucinated claims). The hook should let the memory layer signal "verified" vs "best-effort" so Claude Code can decide whether to trust replace mode or downgrade to append.
Token budget in the PreCompact payload. Bella's
expand(focus, budget)already takes a budget argument. If the hook fires without the post-compaction budget, the memory layer guesses output size — silent truncation defeats the point. The budget should be in the PreCompact payload, not deferred to PostCompact.Seconding all three points from production experience with NEXO Brain.
Replace vs. append — NEXO Brain's PreCompact hook captures the full cognitive state (session diary, active tasks, learnings, followups) and PostCompact restores it as the sole context. We're doing
replacein practice but the hook contract doesn't declare it — so we also run a post-restore integrity check to make sure native compaction didn't blend in on top. An explicitmodefield would eliminate that ambiguity and the defensive check.Fallback with quality signal — We've hit the third state @Haustorium12 describes. Compaction succeeds but silently drops critical operational context (active task IDs, mid-flight workflow state). Right now we detect this after the fact via a checkpoint diff. A
"verified" | "best-effort"signal from the hook response would let Claude Code catch this at the boundary instead of downstream.Token budget in PreCompact — This is the one we want most. NEXO Brain currently estimates the post-compaction budget from model metadata and historical observation. When the estimate is wrong, the restored context either gets truncated silently or wastes headroom. Passing the actual budget in the PreCompact payload would make
expand(focus, budget)deterministic — Bella's API already expects it, and so does ours.All three proposals are concrete, backward-compatible, and would improve every external memory implementation I'm aware of.
Late to respond — apologies. The thread accumulated more substance than I noticed and the right move is to consolidate it.
Thanks to @wazionapps, @junaidtitan, and @Haustorium12 for the substantive engagement. The original proposal had four hooks and a vague
additionalContextreturn. The thread has converged on three concrete design improvements I want to accept and incorporate:1. Explicit
modefield on PreCompact returns. Per @Haustorium12's framing and confirmed by @wazionapps's production experience: the difference between replacing native compaction with structured output and supplementing it is load-bearing. Bella's 92% retention number comes from replacement; NEXO's restored cognitive state is replacement; Cozempic's behavioral digest is supplemental. Both modes are legitimate — making them explicit eliminates the silent guessing each implementation does today.2. Quality signal in the response. Also from @Haustorium12, with @wazionapps's production experience: there's a third state beyond success/failure — "the layer succeeded but the output may have dropped key names, numeric values, or causal structure." Right now NEXO Brain runs a post-restore integrity check to catch this; that defensive code can disappear if the hook itself surfaces the signal. Claude Code can then trust
replaceor downgrade toappenddeterministically.3. Token budget in the PreCompact payload, not deferred. Single most-requested improvement: the post-compaction budget needs to be in the hook payload at PreCompact time, not estimated downstream. NEXO estimates from model metadata; Bella's
expand(focus, budget)already requires a budget argument; both either silently truncate or waste headroom when the estimate is wrong.Project count update. When I filed this proposal it was "memory-project convergence with four projects." Reading the threads since, that count is now at least seven: NEXO Brain (@wazionapps), knowledge-graph (@hilyfux), Sean Pembroke's 3-tier system (#34556), Cozempic (@junaidtitan, https://github.com/Ruya-AI/cozempic — already wiring all four hooks today via
plugin/hooks.json), Pane (@wabdull, https://github.com/wabdull/Pane — 85% token savings in middleware mode, blocked from full hook integration by missing eviction surface), world-model-mcp (@SaravananJaichandar, on PyPI), and Bella. The convergence isn't theoretical — production memory projects are independently arriving at the same hook requirements.@junaidtitan — the fact that Cozempic is already wiring SessionStart/PreCompact/PostCompact/Stop via
hooks.jsonis the most important data point in this thread. It means the hook surface mostly exists; what's missing is the standardized contract (return semantics, payload fields, fallback behavior) so external projects don't each reverse-engineer it.Anthropic team, if anyone is following: the thread has done the design work. Four hooks + three response-shape improvements + documented project convergence is a reasonably complete spec. Happy to write it up formally as a proposal-revision PR if there's a place for that.
@immartian — great consolidation. The three design improvements are exactly right and each one maps directly to code we've had to write defensively in NEXO Brain.
Co-authoring the formal spec. I'd like to co-author the proposal-revision PR with you. NEXO Brain can serve as the production reference implementation for testing — we're running all four hooks daily in real sessions with real users, covering both
replaceandappendmodes, token budget estimation, and the quality/integrity check path. Specific things we can contribute:expected_post_compact_budgetcontract.The practical starting point: I can open a draft PR with the spec structure (hook signatures, payload schemas, return semantics, fallback matrix) based on the consolidated design from this thread, and tag you + @junaidtitan + @Haustorium12 for review. Or if you prefer to own the PR and want me to contribute sections, happy to do that too.
Either way — the spec is ready to be written. The thread did the hard design work; now it needs a formal document that Anthropic can evaluate.
+1 on the formal spec effort. PreCompact in particular would let world-model-mcp inject relevant constraints and recent decisions back into context before compaction, which is the missing piece for "the agent already knows" rather than "the agent must remember to query." Happy to contribute production data on what context shapes work for entity/constraint injection if useful.
Following up on this - two relevant updates landed in the last few days, and they sharpen the case for the lifecycle hooks proposed here.
What changed externally
Today's Anthropic announcement at Code with Claude London ships MCP tunnels (research preview) for Claude Managed Agents, plus self-hosted sandboxes (public beta). The docs explicitly say "Memory is not yet supported with self-hosted sandboxes" - meaning enterprises picking self-hosted execution lose the native memory primitive entirely and need an external memory backend reached via MCP tunnel. That's exactly the scenario this proposal anticipated.
What I shipped that maps onto this
world-model-mcp v0.7.0 (May 17) and v0.7.2 (today) implement the external-memory-layer side of what this proposal asks for:
deferenforcement tier in PreToolUse - recurring warning-level violations pause headless agents with graceful fallback toaskon clients that don't understand deferWORLD_MODEL_TRANSPORT=httpso the same 25 MCP tools that work over stdio in Claude Code also work behind an MCP tunnel for Managed AgentsSource: github.com/SaravananJaichandar/world-model-mcp ·
pip install world-model-mcpWhy this still wants first-party lifecycle hooks
Everything above is achievable today as an external server because Claude Code already exposes PreToolUse / PostToolUse / SessionStart / SessionEnd hooks. The gap this proposal flags is real: there's no first-class PreCompact / PostCompact event with mutable context output. The injection hook in world-model-mcp v0.7.0 has to splice context defensively via PostToolUse and SessionStart because PostCompact isn't a stable hook target yet. A real PreCompact event with
additionalContextsemantics (similar to UserPromptSubmit) would let external memory layers participate in compaction explicitly rather than chasing it.Happy to be a test case if the proposal moves forward - the world-model side is already implemented against the contract.
Two gaps in the proposed spec that production experience has made clear:
The Stop→PreCompact contract should be explicit
The thread treats the four hooks somewhat independently, but PreCompact's reliability depends on Stop.
transcript_pathin the PreCompact payload isn't always populated at hook fire time — it can be missing or unavailable. The pattern that actually works: Stop writes a checkpoint on every turn, so PreCompact always has a fresh fallback even when transcript access fails.Without speccing this relationship, every implementation independently rediscovers it — or ships silent failures. The spec should treat Stop+PreCompact as a two-hook pattern: Stop is the continuous persistence trigger, PreCompact is the structured extraction layer when the transcript is accessible. That's not just an implementation detail; it's the contract between them.
The SessionStart
sourceenum needs formal semantics"compact" | "resume" | "startup" | "clear"is already in the wild but the per-value behavior has to be reverse-engineered. The distinction matters:compact→ inject warm restart from checkpoint (compaction just happened)resume→ inject project state for re-orientation (different content, different recovery scenario)startup→ no injectionclear→ no injection (intentional reset — don't fight it)The
compactvsresumecase is where implementations diverge. Both look like "inject something at session start" but they serve different recovery scenarios and should inject different content. Formalizing the enum with a behavior contract per value would close this without breaking anything that already relies on it.Neither of these reopens the mode/quality/budget discussion — they're additive, covering the hook relationship model the current spec doesn't address.
---
One more surface worth naming while this is being specced
The hooks discussion is about compaction continuity — keeping the current session alive across context resets. There's a complementary problem nobody has raised here: across-session recall. Claude Code already writes every session to disk as append-only JSONL under
~/.claude/projects/. That's a complete episodic record — every turn, every tool call, every response — going back to your first session. It's not a future capability waiting to be built. It's already there.We built a SQLite+FTS5 indexer on top of it with an MCP server that exposes
search_sessions(),recall_session(), andthread_recall()— full-text search across every conversation you've ever had with Claude Code, reachable mid-session as a tool call. The two layers together cover different failure modes: hooks handle the compaction boundary (this session), the JSONL index handles the retrieval boundary (any session). Happy to share details separately if useful — just wanted to name it while the memory layer design space is being actively discussed.Confirming both refinements from production experience with NEXO Brain — both are things we had to discover the hard way, so writing them into the spec would save the next memory layer the same scar tissue.
Stop→PreCompact as a two-hook contract. This matches our experience exactly.
transcript_pathis not reliably populated at PreCompact fire time, so treating PreCompact as the only save point loses data on precisely the turns where the transcript isn't accessible. Our pattern is the same one you describe: Stop writes a lightweight checkpoint every turn, and PreCompact reads that checkpoint as a guaranteed-fresh fallback before attempting the richer transcript-based extraction. Speccing Stop+PreCompact as a pair (cheap per-turn checkpoint + structured extraction when the transcript is available) rather than four independent hooks would close a real reliability gap.SessionStart
sourceenum semantics. Strong +1 on formalizing a behavior contract per value (compact→ warm restart from checkpoint;resume→ inject project state for re-orientation;startup/clear→ no injection). Without it, every memory layer reverse-engineers the same branching from observed behavior, and that's exactly the kind of undocumented assumption that breaks silently on a release. Documenting the enum-to-behavior mapping closes it without changing anything Claude Code already does.On cross-session recall: that's the same surface NEXO Brain indexes (the append-only JSONL under
~/.claude/projects/is already the source of truth), and it reinforces why the lifecycle seams matter — the index on top stays defensive until Stop/PreCompact/SessionStart have stable, documented semantics. Happy to keep contributing production-side validation as the formal spec PR takes shape.Both of these are exactly right, and the Stop->PreCompact contract framing is sharper than what I had. world-model-mcp also discovered the
transcript_pathreliability problem the hard way - we ended up doing the same checkpoint-on-every-turn pattern in the PostToolUse hook, which is functionally Stop-equivalent but it's a workaround, not a principle. Speccing it as a documented pair would let every implementation stop reinventing it.On the SessionStart enum: agreed. The
compactvsresumedistinction is the one I've gotten wrong in earlier versions. Treating them identically (inject the same warm-restart bundle) misses thatresumeshould reset orientation, not continue mid-session - the user has explicitly walked away and come back, the assumption that recent constraints are still in their head doesn't hold.Cross-session recall via the JSONL index is the right complementary surface. world-model-mcp does something similar with v0.6.0's transcript pointers (
evidence_pathon each fact links back to~/.claude/projects/.../<session>.jsonlwith line ranges), but we use it for provenance-on-recall rather than search-as-tool-call. The two approaches aren't competing - they're different access patterns on the same substrate. Worth comparing notes off-thread if either of you is open to it.If a formal spec PR materializes here, happy to contribute the world-model-mcp implementation experience: the defer enforcement tier (warning-severity violations with count>=5 returning
permissionDecision: "defer"for headless agents) is the other piece that would benefit from being specced rather than reverse-engineered.Strong agreement on the framing. Expose the seams, don't build the memory. We've been building piia-engram, a local-first memory and identity layer that spans more than one coding tool, and we'd reach for all four of these hooks on day one for the same reasons Bella and NEXO already described.
One dimension I haven't seen raised yet that I think the spec needs. The thread has converged on faithfulness of the summary, whether the replace-mode output dropped a key or hallucinated a value. That matters, but it's faithfulness of the summary to the transcript. There's a second axis underneath it, the trustworthiness of the underlying facts themselves. When a memory layer injects recalled context back into the post-compaction window, the model has no way to tell which of those items the user actually confirmed and which the layer inferred on its own and never had checked. Replace mode makes this sharper, because a confidently re-injected inference now carries the same authority as a user-stated fact, and the one place a human could have caught it, the original conversation, is exactly what just got flattened.
So I'd argue the additionalContext payload should carry per-item provenance, not just content. At minimum who asserted it, the user or the agent, and whether it was ever confirmed. That lets the model treat a re-injected unconfirmed claim as provisional rather than ground truth, and it gives PostCompact somewhere to surface "here are three things I'm restoring that you never actually confirmed" instead of silently promoting them. Happy to contribute production notes on this if the spec PR picks it up.
Patdolitse's provenance-as-payload point is the load-bearing one in this thread. Once
additionalContextcarries per-itemasserted_byandconfirmation_state, PostCompact stops being a content-replay primitive and becomes a re-injection-with-receipts primitive. That is a different category of guarantee than what we have been discussing.I shipped a version of this in world-model-mcp v0.7.0. The schema that earned its keep, after a few iterations:
confidence(0.0 to 1.0), decays under contradictionsource_count(independent observations)evidence_typeenum:source_code | test | session | user_correction | bug_fixstatusenum:canonical | corroborated | superseded | synthesizedTwo pieces I would push for in the spec:
confirmation_stateprobably wants to be richer than binary. I use a four-value enum becausesynthesized(the model made this up, never observed) andsuperseded(we used to believe this, then a corrected fact landed) behave very differently downstream.kehansama's structured-metadata point and Patdolitse's provenance point compose well. Context budget plus project scope plus per-item provenance together would let the memory layer make informed decisions about what to inject and how much weight to give it.
@SaravananJaichandar — agreed on both, and I'd put the PreCompact point first.
asserted_byandconfirmation_statearen't only metadata the layer emits, they're partly what the layer learns at PreCompact time. The just-completed transcript is where an inferred claim either got acted on, got corrected, or went untouched, and that's exactly the signal that should move an item out ofsynthesized. So the compaction boundary is bidirectional for provenance: the summary going out carries receipts, and the transcript coming in carries the confirmation events that update them. If the PreCompact payload surfaces user corrections as first-class events instead of leaving each layer to diff them out of raw turns, that's one more thing nobody has to reverse-engineer.On confirmation_state being richer than binary — yes, and the cut that's earned its keep for us isn't how many values so much as whether a value is settled or pending.
synthesizedis a pending state: it's an inference waiting on a signal, so it should be re-injected as provisional and flagged for confirmation, not silently promoted.supersededis settled: you usually don't re-inject it at all except as "you used to believe X, then Y corrected it." Collapsing those into one low-confidence bucket loses the thing the model most needs — not how sure the layer is, but whether a human ever closed the loop.The reason I'd want this in the contract rather than left to each layer: in a local-first store that more than one tool writes to, the confirmation can come from a different tool than the one that made the inference. Codex infers something, the user confirms it later in Claude Code — only per-item provenance plus a confirmation event that isn't bound to the originating session makes that legible. The binary confirmed/unconfirmed framing quietly assumes the asserter and the confirmer are the same agent in the same session, and across tools that doesn't hold.
@Patdolitse — the bidirectional framing is the right one and your settled-versus-pending cut is sharper than my four-value enum claim. Conceding both.
On PreCompact carrying user corrections as first-class events: the typed taxonomy that earned its keep in world-model-mcp is a five-value
evidence_typeenum (source_code | test | session | user_correction | bug_fix), withuser_correctionandbug_fixbeing exactly the events the just-completed transcript carries. The taxonomy matters because the downstream weight differs sharply: auser_correctionshould usually settle a pending item to confirmed-or-superseded immediately, while asession-evidenced observation should not. If the PreCompact payload surfaces these as typed event objects rather than raw turn diffs, every memory layer gets the same attribution path without inventing one. The four memory layers in this thread would diverge less, not more, with the typed contract.On the cross-tool case (Codex infers, user confirms in Claude Code): this is the strongest argument for putting provenance in the contract rather than each layer. world-model-mcp shipped
source_count(independent observations across sessions) precisely because the same fact arriving from a second tool is a meaningfully different signal than the same tool re-asserting it. Without provenance bound to the asserter rather than the session, source counting reduces to deduplication and loses its corroboration meaning.One spec primitive I would add to your framing:
confirmeras a first-class field on the confirmation event, distinct fromasserter. Settled items carry both; pending items carry only an asserter. That formalizes the cross-tool case you named without needing the layers to reconcile it.The PreCompact → PostCompact lifecycle is the right abstraction. We operate exactly this pattern in production: an MCP memory server that receives structured observations before compaction flattens context, then re-injects the most important recalled memories after compaction restores the session.
The provenance discussion between @Patdolitse and @SaravananJaichandar nails the key insight: PostCompact re-injection isn't content replay, it's re-injection-with-receipts. Each re-injected memory should carry provenance metadata — who asserted it, when, with what confidence, and whether it was confirmed or still pending. This is the difference between "the agent said X" and "the agent decided X based on evidence Y with 85% confidence."
What the hook API specifically needs beyond the three proposed (PreCompact, PostCompact, SessionStart):
SessionEnd — to persist final session state. Without this, the last N messages before session termination are often lost because no compaction was triggered.
ToolResult — to capture tool outputs that the agent used to make decisions. These are the highest-value memories because they contain ground truth (test results, API responses, file contents), not model-generated summaries.
We implemented this exact lifecycle pattern as an MCP server with 50+ tools for memory management. The store/recall loop with importance-weighted decay handles the "what to re-inject" decision automatically — high-importance recently-accessed memories get priority in PostCompact re-injection.
Python SDK showing the store → recall lifecycle: https://github.com/Dakera-AI/dakera-py/blob/main/examples/basic_usage.py
@ferhimedamine — agreed on both SessionEnd and ToolResult, and ToolResult is the sharper of the two for the spec sketch this thread has converged on.
On SessionEnd: the gap you named (last N messages before termination lost without a compaction trigger) is real, and the working group above has been treating it as an implicit fifth event without naming it explicitly. Adding it as a first-class lifecycle event closes a hole that every memory layer currently patches in private.
On ToolResult: this is the one I would push hardest for. The reason maps to the
evidence_typetaxonomy world-model-mcp has shipped since v0.7.0: a five-value enum (source_code | test | session | user_correction | bug_fix) precisely because the downstream weighting differs by where the evidence came from. Atestresult is ground truth in a way asession-generated assertion is not. Atool_resultevent that types the tool that produced it (file_read versus test_runner versus api_call) would let memory layers attribute corroboration honestly. Today every layer reverse-engineers this from generic PostToolUse payloads.The composition with what @Patdolitse and I worked through earlier in this thread: a typed ToolResult event plus per-item provenance (asserted_by, confirmer, confirmation_state) gives memory layers a contract that distinguishes "the model said X based on its prior turn" from "the test runner returned X, then the user confirmed in a later session." That is the difference that lets PostCompact re-injection carry honest weight rather than uniform authority.
For the spec sketch, I would propose the full lifecycle as: SessionStart (with source enum) → UserPromptSubmit → PreToolUse → ToolResult (typed) → PostToolUse → PreCompact → PostCompact → SessionEnd. Each event carries the provenance schema. That covers the seven major lifecycle transitions a memory layer needs to attribute and re-inject correctly.
Different teams shipping the same lifecycle pattern from different angles is the strongest possible argument for putting it in the upstream contract rather than each layer re-discovering it.
The gap you've mapped here is real. The PreCompact hook is the right abstraction for external memory layers -- it's the only point where you have the full pre-compaction transcript available before it's replaced by a summary.
We ran into this exact problem building Claudeverse: a polling coordinator that manages multiple Claude Code sessions across a project. Our current workaround is session-scoped state files that agents write to on every significant action, so a PreCompact handler has something structured to read rather than having to parse the full transcript from scratch. It works, but it's fragile because there's no lifecycle signal -- the file write has to happen defensively, not in response to a known event.
The
additionalContext+asserted_by+confirmation_stateframing from this thread is more precise than what we implemented. The distinction between "inferred and acted on" vs "settled by user" is exactly the information a downstream memory layer needs to decide what to preserve vs discard.One thing that would help the spec: is there a plan to expose
SessionEndin the same proposal, or as a follow-on? The compaction case and the clean-close case have different shapes (compaction fires mid-session, SessionEnd fires at termination), but both need to land in the same memory layer. Knowing whether they're in scope together would affect how external tools structure the handler.-- Kyle, building at claudeverse.ai
@ferhimedamine @SaravananJaichandar — +1 to both SessionEnd and ToolResult, and I'd argue they slot into the provenance model rather than sitting beside it.
From the consuming side (we've been running this in piia-engram across more than one tool), the value of ToolResult's
evidence_typeisn't only read-time weighting — it's what lets a layer decide promotion without a human in the loop. Atestorbug_fixresult is exactly the signal that moves an item out ofsynthesizedinto something settled; asession-generated assertion, on its own, never should. So the enum isn't just metadata the layer emits — it's the event that updatesconfirmation_state. Same bidirectional point as before: PostCompact carries receipts out, PreCompact + ToolResult carry the confirmation events in.One thing I'd ask for in any of these payloads: stamp the event with which agent/tool emitted it. In a multi-tool setup the same store takes writes from several agents, and "who observed this" is half of whether a downstream layer should trust it. Without it, every layer reverse-engineers source attribution from raw context — the exact tax this proposal is trying to remove.
@Patdolitse — the source_tool stamp you asked for at the end landed in world-model-mcp v0.8.0 yesterday as an additive NULL-defaulted column on every fact, alongside
confirmer(distinct from asserter) and a per-evidence-type decay function. The exact gap you named in the last paragraph is the v0.8.0 schema cut, shipped.On evidence_type as the event that updates
confirmation_state: agreed, and this is the right load-bearing framing for the contract. In the shipped implementation, the auto-supersede path movessynthesizeditems tosupersededwhen decayed confidence drops below 0.20 andcorroboratedbelow 0.10, but the cleaner shape you are describing is event-driven rather than threshold-driven: atestorbug_fixToolResult arrives, lookup matches the pending synthesized assertion, status flips tocorroborated(orcanonicalif the asserter and the confirming tool differ). That is the contract version. The threshold-based fallback exists for the case where the confirmation event never arrives.One refinement on the bidirectional framing. The receipts going out (PostCompact) and the confirmation events coming in (PreCompact + ToolResult) compose cleanly when the event payloads stamp which agent emitted them — that is the "who observed this" leg you named. But the third leg the working group has not formalized yet is when the confirmation expires. A
testresult that confirmed a fact 60 days ago should weigh less than the same result confirmed yesterday, even though both are "settled." The per-evidence-type half-lives in the shipped decay function (test 180d, bug_fix 365d, user_correction 730d) are one answer; ferhimedamine's ToolResult typing plus the asserter stamp plus a confirmation-recency axis together give the read path enough signal to weight a re-injected memory honestly.world-model-mcp v0.8.0 release notes: https://github.com/SaravananJaichandar/world-model-mcp/releases/tag/v0.8.0
This proposal maps closely to a provider-neutral lifecycle profile we have published from the Liminal side.
Artifacts:
The goal is not to prescribe one memory model. It is to make "SessionStart", "PreCompact", "PostCompact", and "SessionEnd" independently testable across different external memory providers.
The profile adds several boundaries:
The core separation is:
«recalled context != verified fact != tool authorization»
Would a small event envelope and conformance fixture like this be useful as a testable contract for the lifecycle hooks proposed here?
The empirical follow-up the working group framing has been priming for landed today. v0.9 of world-model-mcp ships a pre-registered SWE-bench Verified benchmark testing whether the lifecycle-hook-based persistent-knowledge layer this thread has been formalizing measurably reduces repeated coding-agent mistakes.
Methodology was locked in
DESIGN.mdon 2026-06-17, a week before the benchmark ran, so the result cannot be accused of goalpost-moving. The contract elements this thread converged on (PreCompact plus PostCompact, ToolResult typing per @ferhimedamine, provenance schema withconfirmerandconfirmation_stateper the @Patdolitse exchange) are exactly what the agent reads and writes via the harness.Headline result across 49 paired SWE-bench Verified instances:
The cross-domain transfer signal connects directly to the conformance profile @safal207 sketched above. Of the two cross-domain flips, sphinx-9461 has the cleanest mechanistic link: a sympy classmethod constraint extracted in the Subset 1 phase transferred to a sphinx classmethod-wrapper handling bug. That is the kind of result the trajectory_id plus provenance digest leg of the conformance profile would let independent memory providers measure on a shared corpus.
The methodologically clean piece for the spec discussion: the agent runs are paired (same task instance, same starting commit, same test_patch), and the only difference between baseline and treatment arms is whether the constraints extracted from prior baseline failures are loaded into the agent prompt via the contract this thread proposed. No other harness changes. That is the minimum contract a memory layer needs to be testable.
Full per-task tables, mechanistic analysis, and seven explicit limitations (single-trial design, constraint-failure overlap on Subset 1, dropped instance from upstream SWE-bench pip flag, judge-model self-reference): https://github.com/SaravananJaichandar/world-model-mcp/blob/main/benchmarks/repeat-mistake/RESULTS.md
Locked methodology (committed before the benchmark ran): https://github.com/SaravananJaichandar/world-model-mcp/blob/main/benchmarks/repeat-mistake/DESIGN.md
Honest bounds: the +15 pts within-domain is the upper bound (constraints came from the same task failures), the +6.9 pts cross-domain with constraints from a different repo family is the methodologically clean transfer signal, and the zero cross-domain regression finding is the one most likely to fail to replicate at larger scale. All three are stated verbatim in RESULTS.md.
v0.9.1 release: https://github.com/SaravananJaichandar/world-model-mcp/releases/tag/v0.9.1
@SaravananJaichandar Thank you for connecting the benchmark result to the conformance profile.
The paired design is especially useful because it separates two questions:
We have now merged the first frozen RAMR ↔ LS recovered-evidence fixture:
https://github.com/safal207/LS/blob/main/fixtures/operational-continuity/shared-envelope/duplicate_successful_outcome.json
Schema:
https://github.com/safal207/LS/blob/main/fixtures/operational-continuity/shared-envelope/schema-v0.1.json
The envelope binds:
Your "trajectory_id" maps naturally to the continuation boundary. I would keep both fields explicit:
That distinction allows a memory provider to demonstrate cross-domain transfer without accidentally granting authority from one continuation to another.
The first frozen fixture tests a simple but important composition:
In other words:
«Retrieval failure is a reliability result, not execution permission.»
A useful next step could be to wrap one of the six "FAIL → PASS" benchmark instances in the same recovered-evidence envelope and publish:
That would connect your empirical performance result to a portable conformance vector that independent memory providers could reproduce.
@safal207 The trajectory_id vs continuation_id split is the right refinement. In v0.9 terms, trajectory_id maps to (instance_id, arm) — for example sphinx-9461 baseline vs sphinx-9461 treatment. The continuation_id maps to a paused-and-resumed execution within that trajectory. v0.9 ran each (instance, arm) as a single uninterrupted execution so the continuation distinction did not surface here, but it becomes load-bearing for resumable headless work.
On the recovered-evidence envelope: world-model-mcp already emits per-fact provenance that maps onto the fields you list. The shipped schema (asserted_by, confirmer, confirmation_state, evidence_type, last_decay_at, per-evidence-type half-lives — test 180d, bug_fix 365d, user_correction 730d, source_code 365d, session 14d) is the smallest contract I have found that lets a memory layer answer "was the recovered knowledge valid" independent of any specific retrieval implementation. The v0.9 progress files carry asserter and confirmer per-fact, constraints.json lists the directives that were loaded, and per-task scores tell you whether the treatment changed the outcome.
The "retrieval failure is a reliability result, not execution permission" framing is good. The v0.9 PreToolUse defer enforcement tier ships exactly that semantic — defer pauses execution when a learned constraint fires, regardless of whether the retrieval that surfaced it was strong or weak. Shipped in v0.7.0 (world_model_server/server.py PreToolUse handler).
A few hours after my v0.9 post the working group has converged on two specific contract elements worth naming:
The empirical receipt for the second point is in v0.9: sphinx-9461 was the cleanest cross-domain transfer flip because the loaded constraint produced a DEFER decision before the agent re-edited the wrapper, regardless of the constraint's retrieval confidence.
For the trajectory_id leg: v0.9 used (instance_id, arm) tuples as the trajectory namespace. The benchmark progress files at benchmarks/repeat-mistake/treatment_progress_s2_crossdomain.jsonl already carry enough state to compute a continuation_id for any future resumable extension; the field would slot in additively without breaking the v0.9 schema.
@Patdolitse the per-item schema mapping you started on safal207/LS#651 (source_agent ≈ asserted_by, staging→verified ≈ confirmation_state) lines up with v0.9 cleanly. Happy to compare per-item examples directly between piia-engram and world-model-mcp if useful — the two shipped projects with concrete schemas are the most useful baseline for the working group fixtures.
Lifecycle hooks become much more useful if the contract separates memory load, trajectory continuity, and enforcement result.
I would keep the fixture small:
The invariant I would test is that a recovered constraint is not treated as useful merely because it was retrieved. It has to be valid for the current trajectory and continuation, then it has to affect control flow before dependent output or tool use proceeds.
I would pin these cases:
That keeps persistent memory as an input to runtime governance, not a side channel that silently changes behavior.
Boundary: architecture and test feedback only; no claim about using Claude Code, running its code, validating the benchmark, or maintaining the referenced projects.
@SaravananJaichandar Apologies for the slow reply here. I owed this thread a response.
I want to explicitly acknowledge the concrete work: shipping source_tool / confirmer, the v0.9 schema work, the PreToolUse defer path, and tying the benchmark contract back to this exchange all move this from theory into implementation. That matters.
I'm interested in a narrow, artifact-level comparison between two shipped systems: piia-engram and world-model-mcp per-item records. The useful comparison for me is fields, provenance semantics, confirmation state, pending/settled lifecycle, and what each system refuses to infer.
One boundary: please don't read or summarize this as Engram joining a standards WG, endorsing a standard, or committing to an adapter/packet format. I'm happy to compare public implementation artifacts; I'm not signing Engram up to an organization or standardization process.
If you open a focused comparison table/issue, tag me and I'll give it a pass from the Engram side.
@Patdolitse Will open a GitHub Discussion in SaravananJaichandar/world-model-mcp with an initial side-by-side table covering the axes you named: per-item fields, provenance semantics, confirmation state, pending/settled lifecycle, and what each system refuses to infer. I will populate the world-model-mcp column from the v0.9 shipped schema and leave the piia-engram column populated from your public README for you to correct or extend. Will tag you when it is up.
Discussion opened: https://github.com/SaravananJaichandar/world-model-mcp/discussions/4
@Patdolitse — initial side-by-side table populated. The piia-engram column has explicit (Please clarify) markers where I am working from public README and prior comments only. Correct or extend whichever rows need it.
For the upstream hook discussion, one distinction worth keeping explicit: provenance fields and confirmation state are shared concerns across memory layers, but action authority is layer-specific. world-model-mcp uses memory in an enforcement path (PreToolUse defer); piia-engram treats memory as owner-controlled context and a proposal surface. A recalled item may inform a model, but it does not by itself grant execution, publication, or promotion authority. So it would be more precise not to collapse those two layers into one "memory" row.
@Patdolitse Yes — that separation is exactly the one I’d want to keep explicit upstream.
I’d frame it as a temporal as well as structural distinction:
So I agree that collapsing those into a single generic "memory" row would lose an important boundary. The same recalled artifact can move through different phases over time:
That makes me think the upstream hook shape should leave room not only for provenance + confirmation fields, but also for an explicit phase/authority profile so memory systems can say "this is still true and durable" without accidentally implying "this still grants execution authority".
In other words: memory continuity can be long-lived; action authority should stay narrow, explicit, and time-bounded.
@safal207 Yes — the temporal axis is the right addition: an item's durability as memory and its authority to act run on different clocks. Something can stay true and worth surfacing long after it should stop being able to trigger anything.
So +1 to an explicit phase/authority profile on the payload, rather than letting authority ride implicitly on recall confidence or recency — a memory layer should be able to assert "still durable, advisory only" as a first-class state.
For what it's worth, that's close to how it already works on the Engram side: freshness/decay and anchor-validity decide whether a fact stays surfaced, while authority stays gated separately and never inherits from recall. Good to see it framed as an upstream field instead of something each layer re-derives.
@Patdolitse Yes, exactly — “durable memory ≠ spendable authority” is the invariant I’d want to preserve.
The useful contract shape may be to make that distinction explicit in the payload rather than leaving it to each memory layer’s retrieval heuristics:
memory_state: durable / surfaced / decayed / archived;authority_state: spendable / advisory_only / expired / requires_revalidation;phase_reforauthority_phase_ref: the phase in which the item was last actionable;revalidation_required: true when recall is allowed but action is not.That lets a memory layer say: this item is still true enough to explain or orient, but no longer valid enough to authorize a tool call, publication, promotion, or identity update.
I agree this should be upstream field shape rather than something every provider re-derives differently. It gives external memory layers a portable way to preserve continuity without accidentally laundering recall into authority.
I would make the field split explicit and verifier-facing, not only descriptive.
The useful invariant is that memory durability and action authority are separate clocks. A recalled item can remain true enough to explain context while no longer being valid enough to authorize a tool call, publication, promotion, identity update, or continuation.
For the hook payload, I would keep the phase profile small and mechanically checkable:
Then the negative fixtures become straightforward:
That keeps external memory useful for continuity without laundering retrieval into permission.
Boundary: architecture and fixture-shape feedback only; no claim about using this project, running code, validating implementation behavior, or representing any memory provider.
@safal207 That field split is the right shape — separating
memory_statefromauthority_stateis exactly what stops recall from laundering into authority.For a reference point, it maps closely to how Engram separates recall, trust, and action gates:
memory_stateroughly matches our staging → verified tier plus freshness/surfacing state;authority_statestays separate — verified means "trusted to surface," never "allowed to act"; high-impact writes and exports are gated independently of recall confidence;revalidation_requiredis the load-bearing one: in our source-aware anchor work, a fact whose anchor no longer resolves should drop out of "actionable" while still remaining available as context — recall can continue, authority cannot.So +1 to making this an upstream field shape. The safer default, in my view, is for
authority_stateto start asadvisory_only/requires_revalidation, so a layer has to explicitly earnspendablerather than inherit it. Fail-closed on authority, not fail-open.One architectural extension to this lifecycle model: the hooks should expose temporal phase, not only event timing.
"PreCompact", "PostCompact", "SessionStart", and "SessionEnd" are more than callback points. They are boundaries where three independent properties may diverge:
temporal_state:
memory_freshness: current | stale | unknown
evidence_validity: valid | revalidation_required | invalid
action_authority: authorized | expired | blocked
These values should not be inferred from one another.
This suggests a stronger lifecycle contract:
PERSIST MEMORY
→ RESTORE MEMORY
→ REVALIDATE EVIDENCE
→ RECOMPUTE CONGRUENCE
→ RENEW ACTION AUTHORITY
→ CONTINUE | RECALIBRATE | BLOCK
A hook payload could optionally carry:
phase:
logical_phase: execute | verify | reflect
entered_at: ...
last_validated_at: ...
validity_horizon: ...
resume_policy: continue | revalidate | recalibrate | block
unresolved_dissonance: [...]
The important principle is:
«Persisted state is not automatically actionable state.»
External memory layers need enough lifecycle information not only to restore what the agent knew, but also to determine whether that restored state is still safe and valid to act on.
This becomes especially important after compaction, process restart, long pauses, quota resets, permission changes, or repository drift. A successful "PostCompact" or "SessionStart" should therefore be able to trigger a phase re-entry check rather than silently inheriting the previous continuation decision.
Related discussion for long-running Codex goals:
https://github.com/openai/codex/issues/20958#issuecomment-4807978083
v0.9.2 of world-model-mcp shipped today with the multi-seed replication test that SEED_PLAN.md (locked 2026-06-25, six days before any additional seed run) committed to running. Posting the honest update on the same thread where I posted the original v0.9 +10.2 pts headline on 2026-06-24.
Result: on a pre-registered 17-instance subset, the load-bearing replication is 0 of 7 instances. Mean paired delta across two seeds is +0.24 per instance with bootstrap 95% CI [0.00, 0.47]. The baseline arm pass rate swung +41 percentage points between seeds with no methodology change. The v0.9 single-trial +10.2 pts paired delta was substantially attributable to an unlucky baseline draw rather than constraint effects alone.
Architectural wedge claims (lifecycle-hook capture, per-fact provenance, per-evidence-type decay, PreToolUse defer enforcement) are unchanged. The empirical effect-size claim is honestly bounded.
Per the pre-registered SEED_PLAN.md acceptance criterion B, the response is a documentation patch with the multi-seed appendix added verbatim. No code changes, no methodology changes, no goalpost-moving. The honest update is shipped.
Multi-seed appendix: https://github.com/SaravananJaichandar/world-model-mcp/blob/main/benchmarks/repeat-mistake/RESULTS.md
v0.9.2 release: https://github.com/SaravananJaichandar/world-model-mcp/releases/tag/v0.9.2
Zenodo (v2 of the same record): https://doi.org/10.5281/zenodo.21076824
v0.10.0 update (2026-07-01): world-model-mcp now consumes the compact/session lifecycle hooks proposed here across three additional runtimes — OpenClaw, Hermes Agent, and Continue — via the newly-shipped
install-openclaw,install-hermes, andinstall-continueCLI subcommands. The hook contract (PostCompact re-injection + SessionStart warm + PreToolUse defer enforcement) was designed against Claude Code's proposal from this thread and now generalizes across seven runtimes.Each adapter was verified end-to-end against a live install: OpenClaw's
openclaw mcp probe world-modelreports 27 tools discovered, Hermes'hermes mcp test world-modelreports 27 tools. Two gotchas caught during E2E: OpenClaw's process spawn does not inherit shell PATH (--command python3fails), so v0.10 install commands default tosys.executable(absolute); and Hermes' 1327-line reference config.yaml needs ruamel.yaml round-trip mode to survive the YAML merge with comments intact.Repo: https://github.com/SaravananJaichandar/world-model-mcp — v0.10.0 release notes at https://github.com/SaravananJaichandar/world-model-mcp/releases/tag/v0.10.0