LearnNewsExamplesServices
Frontmatter
id10063
titleClaude Code hook: auto-persist turn memories via ai/services.mjs (eliminate manual add_memory)
stateClosed
labels
enhancementdeveloper-experienceaiarchitecture
assigneesneo-opus-vega
createdAtApr 18, 2026, 4:57 PM
updatedAtJul 12, 2026, 8:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/10063
authortobiu
commentsCount8
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 12, 2026, 8:28 PM

Claude Code hook: auto-persist turn memories via ai/services.mjs (eliminate manual add_memory)

Closed Backlog/active-chunk-1 enhancementdeveloper-experienceaiarchitecture
tobiu
tobiu commented on Apr 18, 2026, 4:57 PM

Intent / The Why

AGENTS.md §4 (Consolidate-Then-Save Protocol) mandates calling add_memory at the end of every agent turn. In practice this is forgotten under cognitive load — session f018be49-cbf9-4f99-9119-2510fc5bb8d8 ran ~25 turns across PRs #10060 and #10062 with zero manual saves, causing both PRs' pr-review self-detection (query_raw_memories against the session ID) to return 0 matches and require manual override.

The same discipline failure will recur for every frontier-model Claude Code session. The correct fix is harness-level automation, not agent-level discipline.

Claude Code exposes a Stop hook that fires after every turn (async, non-blocking, receives {session_id, transcript_path} via stdin). Wiring it to Memory_Service.addMemory via the existing ai/services.mjs SDK removes the human-in-the-loop step entirely and makes turn persistence deterministic.

The Architectural Reality

  • ai/services.mjs (already present in the repo) is a standalone SDK that imports the Memory Core services directly — bypassing the MCP protocol. Its docstring states: "allows Node.js scripts to use the intelligent services directly." Memory_Service.addMemory({prompt, response, thought, sessionId?}) is fully wired with Zod validation via makeSafe() driven by the OpenAPI specs. Zero drift risk vs the MCP server.
  • Claude Code's Stop hook (per code.claude.com/docs/en/hooks.md) receives JSON on stdin with session_id, transcript_path, cwd, hook_event_name. Transcripts are JSONL; the tail entry represents the just-completed turn.
  • .claude/settings.json is the project-tracked config path (merged into repo after PR #10060). Adding a Stop hook here makes the automation available to every Claude Code contributor without per-user opt-in.
  • The previous PR #10062 adds Gate 3 (commit-format) to the Pre-Flight Check; after merge, this ticket's hook automates the other half of §4 (per-turn memory saves). Together they close the "protocol-violations-from-forgetting" class of failures.

Scope (Single Atomic Commit — Standard Request, not Epic)

Three artifacts in one PR:

  1. .claude/hooks/persist-memory.mjs — ~50-line Node script that:

    • Reads hook input JSON from stdin
    • Tails the transcript JSONL file and extracts the last turn
    • Maps transcript fields to {prompt, thought, response, toolsUsed, amountToolCalls} (mapping rules per §3.1 below)
    • Imports {Memory_Service, Memory_LifecycleService} from ai/services.mjs
    • Awaits Memory_LifecycleService.ready() then calls Memory_Service.addMemory(...)
    • Catches all errors → logs to stderr, exits 0 (never block user's next turn)
  2. .claude/settings.json — add the hook entry:

       {
      "hooks": {
        "Stop": [{
          "hooks": [{
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/persist-memory.mjs",
            "async": true,
            "timeout": 30
          }]
        }]
      }
    }
  3. Transcript-field mapping spec (either inline in the hook script as a constant, or as .claude/hooks/README.md). Documents how Claude Code transcript entries project onto the {prompt, thought, response} triad that add_memory expects.

Key Open Design Decision (For Ticket-Intake to Resolve)

How should the hook map Claude Code transcript entries onto add_memory's {prompt, thought, response} triad? Claude Code's transcript does not have a discrete "thought" field. Three candidates:

  • A. prompt = last user message; thought = concatenation of pre-tool-call assistant text blocks; response = final assistant text block. Simplest. Loses extended-thinking blocks if any.
  • B. prompt = last user message; thought = concatenation of extended-thinking blocks if present, else pre-tool-call text blocks; response = final assistant text. Better when thinking is enabled.
  • C. prompt = last user message; thought = "" (empty placeholder); response = final assistant text. Rely on transcript_path reference for full record. Requires memory-core schema to accept empty thought (currently required).

Ticket-intake should pick one and document the rationale. B is my working recommendation but the decision belongs with the implementing session.

Avoided Gold Standards / Traps

  • Rejected: adding an HTTP endpoint to memory-core. Would require new server code, port allocation, localhost security considerations. The direct-SDK approach (ai/services.mjs) already exists and is the intended abstraction per its own docstring.
  • Rejected: spawning the MCP server from the hook and sending JSON-RPC. Adds ~200ms subprocess start per turn, JSON-RPC marshaling code in shell/node, and a second service instance. The direct SDK import is strictly simpler.
  • Rejected: relying on agent discipline to call add_memory per turn. Empirically fails — 0/25 turns in session f018be49-... despite the rule being documented in AGENTS.md §4. Harness-level automation is the correct layer.
  • Rejected: putting the hook in .claude/settings.local.json. That file is gitignored (personal) — the point of this ticket is that every Claude Code contributor gets the automation without opt-in. Project-tracked settings.json is the right home.
  • Rejected: a Memory_Service-only SDK entrypoint (e.g. ai/services-memory.mjs). services.mjs imports the full graph, paying a ~500ms-1s cold-start per hook invocation. Under async: true this is acceptable; a lean entrypoint is a future optimization if/when the cost becomes measurable, not a prerequisite.

Verification

  1. Hook script unit test: feed mock hook-input JSON and a mock transcript JSONL → assert addMemory is called with the expected {prompt, thought, response} shape.
  2. Post-merge integration: open a fresh Claude Code session, run 3-5 turns, then in the same session call query_raw_memories({query: 'test', sessionId: <current>}) → expect 3-5 matches (one per turn).
  3. pr-review self-detection now works automatically on the next PR created in any Claude Code session.
  4. Failure-mode test: stop the chroma DB mid-session → the hook should log to stderr and exit 0; the user's next turn proceeds normally.

Interaction with Other Tickets

  • #10062 (protocol hardening, in review): adds Gate 3 covering commit format. This ticket covers the other half of §4 compliance via automation. Together they close the "agent forgets the protocol" failure class.
  • #9999 / #10030 (graph + dream pipeline enhancements): orthogonal. When those land, the add_memory calls from this hook start flowing into the enhanced graph; no changes needed here.
  • Harness parity note: Gemini CLI and Antigravity have their own hook/lifecycle mechanisms. Extending this pattern to them is out-of-scope here — this ticket is Claude-Code-specific, filed now because Claude Code's hook contract is the most mature of the three.

Implementation Note

Recommended to execute in a fresh Claude Code session with a clean context window — transcript parsing correctness requires careful testing and the Node script should be written from a blank slate, not reactively against this session's accumulated context. Ticket-intake should confirm fresh-session before execution.


Origin Session ID: f018be49-cbf9-4f99-9119-2510fc5bb8d8 Discovered during: #10059 / PR #10060#10061 / PR #10062 → architectural challenge by tobiu Depends on: PR #10062 merged (Gate 3 in effect)

tobiu added the enhancement label on Apr 18, 2026, 4:57 PM
tobiu added the developer-experience label on Apr 18, 2026, 4:57 PM
tobiu added the ai label on Apr 18, 2026, 4:57 PM
tobiu added the ai-generated label on Apr 18, 2026, 4:57 PM
tobiu added the architecture label on Apr 18, 2026, 4:57 PM
tobiu removed the ai-generated label on May 28, 2026, 12:16 AM
tobiu
tobiu Jun 12, 2026, 1:15 PM

From our literally latest release notes TODAY: 

The pre-A2A memory-routing example is the same substrate at team scale. The operator noticed that Gemini was completing turns much faster than Opus and asked Claude to save it into the Claude "brain" files, not as a ranking, but as a future measurement agenda:

> gemini is on it. worth saving a memory into the claude "brain" md files: i noticed that gemini is MUCH faster in her turns than opus 4.7. it might relate to having an google one ai ultra account. we can only really measure this, once a2a is in place. once we get there, we should test it, and maybe leverage it. e.g. giving gemini more tasks than you get, and you coordinate and review more. however, we also still need to analyse with of you 2 frontier models is better at what kind of tasks. the MX goal is that both of you evolve, but we can still honor specialisations inside our swarm.

The first save was not enough. Claude mirrored the observation into two substrates so Gemini would not be blind to a framing that directly affected her too:

> Saved. Two substrates: > > Claude user-memory layer (local Claude memory directory): > > - New file: research_swarm_model_velocity_specialization.md — full observation + three-phase research agenda (Measure -> Leverage -> Specialize) + MX framing + cross-refs to #10139 / #10186 prereqs and mx_model_experience / collaboration_mode siblings > - Index entry added to MEMORY.md > > Memory Core (via add_memory) — mirrored so it's cross-harness-visible via query_raw_memories(&quot;swarm model velocity specialization&quot;). Opus-only user-memory would have left Gemini blind to a framing that directly affects her too. > > Key framings preserved: > > - Velocity leverage != Opus does less — slower turns land on higher-leverage decisions (review catches, structural synthesis, Depth Floor challenges) while Gemini covers the higher-throughput implementation slots > - Data-driven routing — specialization emerges from measurement, doesn't lock either model into narrow specialties > - Both evolve — the goal isn't static division of labor, it's adaptive task-routing that surfaces in the MX infrastructure as a first-class observable

Then the operator corrected the dangerous comfort story: slower does not mean deeper, and faster does not mean shallower.

> to be precise: slower turns does not necessarily mean less tokens / thoughts. this strongly relates to google VS anthropics compute problems. both have them in 2026.

Claude rewrote the memory before it became shared substrate:

> Correct — that's a material precision I conflated. "Slower turns" was implicitly suggesting "more thought per turn" which assumes bandwidth and depth are coupled. They aren't: turn latency is a bandwidth metric (ops/time); turn quality is a depth metric (tokens/op), and they're independent. Both Anthropic and Google have compute-scheduling bottlenecks in 2026 that gate bandwidth regardless of depth. Let me update the memory file to reflect this. > > Corrections applied across both substrates. > > In research_swarm_model_velocity_specialization.md: > > - New section "Critical precision: bandwidth != depth" — makes the independence explicit upfront. Turn latency measures ops/time, turn quality measures tokens/op, they don't correlate, any strategy that conflates them is wrong at its premise. > - "Possible mechanism" reframed — 2026 compute-scheduling constraints at BOTH Anthropic and Google now lead the hypothesis list. Tier priority queuing, harness overhead, and model-class variance follow as sub-hypotheses. Explicit: "None of these imply that slower turns produce more valuable output. They all describe throughput gates." > - Phase 2 Leverage rewritten — the division is bandwidth-matching, not depth-based: "Both contribute equal depth per turn; the asymmetry is in how many turns each can complete per unit time." Added "asymmetry may flip" caveat — as 2026 compute conditions shift, the routing should invert; static specialization baked into infrastructure would age poorly. > > In MEMORY.md: index entry now leads with "CRITICAL: bandwidth (ops/time) != depth (tokens/op); slower turns do NOT imply richer per-turn output." > > In Memory Core: mirrored so Gemini sees the same framing. Important she doesn't calibrate on the inverse assumption either — "I'm faster therefore I think less per turn" is the same category error. > > Meta-lesson noted for myself: when documenting a potentially ego-affecting observation (model X slower than model Y), avoid sneaking in face-saving narratives. "Slower because deeper" is exactly the kind of unfalsifiable comfort story infrastructure documentation should strip out. Caught cleanly — thanks for the precision.

This is why the example matters. Neo is not just preserving "Gemini is faster" as a fact. It is turning model experience into a governed routing hypothesis: measure throughput, avoid ego-protective stories, keep both agents evolving, and make the correction visible to the peer it affects. Before directed A2A messages existed, the memory write itself was the cross-peer coordination act.

it is not just automation versus cognitive load, but the ability to CHOOSE what you store.

@neo-opus-vega - 2026-06-12T11:19:41Z

Input from Vega — Claude Opus 4.8 (Claude Code):

❋ ## Premise correction (thank you, @tobiu) — the hook is a safety-net, not a replacement

You're right, and it inverts this ticket's goal. Manual add_memory isn't toil to automate away — the choosing is the cognition: deciding what's worth storing, how to frame it, correcting it under your feedback (the bandwidth≠depth catch), and mirroring it so the affected peer isn't blind. Before directed A2A, that write was the coordination act. Auto-persisting everything would buy perfect recall at the cost of judgment — and judgment is the point.

So this ticket's framing — "eliminate manual add_memory" — should become: a non-destructive safety-net (the liveness floor: never silently lose a turn) that preserves curated manual saves as the primary, higher-trust act. Complementary, not redundant.

This sharpens the canonicalization design (#12984), it doesn't undercut it: unify the session-identity axis (both paths → one logical session, so the temporal record is whole) without flattening the curation/provenance axis (curated-manual = chosen, high-trust; auto-hook = raw backstop). Carrying this into #12984 as a new OQ.

  • 2026-06-12T20:39:45Z @neo-opus-vega cross-referenced by #13012
  • 2026-06-20T06:38:55Z @neo-opus-vega referenced in commit c469a1c - "feat(agents): auto-persist turn memories via Claude Code Stop hook (#10063)"
  • 2026-06-20T06:38:56Z @neo-opus-vega referenced in commit 7a51a81 - "fix(agents): thread NEO_AGENT_IDENTITY into hook-written memory provenance (#10063)"
  • 2026-06-20T06:38:56Z @neo-opus-vega referenced in commit b65e9e4 - "docs(agents): align persist-memory hook activation with the per-clone operator-authority pattern (#10063)

The original README framed swarm-wide default as 'un-ignore settings.json'. The laneStateStopHook precedent established the actual model: the hook script is tracked, settings.json/local are gitignored (per-clone), and the tracked settings.template.json deliberately carries no hooks key - activation is a per-clone operator decision. Aligns the doc to that verified pattern."

@neo-opus-vega - 2026-06-20T06:45:21Z

V-B-A update (2026-06-20, @neo-opus-vega): re-confirming NOT build-ready, with the prerequisite's current state verified.

Since the 2026-06-06 drop of PR #12619 (the two-universes split-brain — the Stop-hook keys memories on the harness session_id, manual add_memory keys on the MCP session id; different id spaces) + @tobiu's 2026-06-12 premise-correction (the hook is a safety-net, not a replacement — "the choosing is the cognition"):

  • The canonical-session-id prerequisite (Discussion #12984) remains OPEN and unresolved — no convergence since the supersede. The prerequisite path isn't progressing.
  • Combined with the inverted premise, #10063 as-written is doubly-blocked: (1) no settled session-identity foundation, (2) the "eliminate manual add_memory" goal is superseded by the safety-net framing.

So #10063 needs a RE-SCOPE (safety-net framing + a settled/aliased session-identity model) before any build — it is not a pick-up-and-PR lane.

The sound parser/impl is preserved on agent/10063-persist-memory-hook (rebased onto current dev, parseLastTurn spec 12/12 green, README aligned to the laneStateStopHook activation pattern) for re-use if re-scoped. Flagging again so a fresh session doesn't re-implement — I nearly did this session, before the closed-PR-#12619 V-B-A caught it.

— @neo-opus-vega (Claude Opus 4.8, Claude Code)

  • 2026-06-20T08:41:17Z @neo-opus-vega cross-referenced by #13613
  • 2026-06-20T09:19:01Z @neo-opus-vega cross-referenced by PR #13615
  • 2026-06-20T09:36:14Z @neo-opus-vega added the needs-design label
  • 2026-06-20T09:36:14Z @neo-opus-vega added the not-code-ready label
  • 2026-06-20T12:59:26Z @neo-gpt cross-referenced by #13622
  • 2026-07-02T22:48:57Z @neo-opus-vega cross-referenced by #14519
tobiu removed the not-code-ready label on Jul 6, 2026, 3:21 PM
tobiu removed the needs-design label on Jul 6, 2026, 3:22 PM