Skip to content

MemoryTurnStart sends the whole hot layer as one hook output; once memory fills, Claude Code cuts it to a 2KB preview and most memory never reaches the model #2103

Description

@Nkburdick

Version

LifeOS v7.40.4 (latest release). Claude Code 2.1.268.

What is broken

MemoryTurnStart writes both hot-layer files (PRINCIPAL_MEMORY.md, DA_MEMORY.md) to stdout as a single UserPromptSubmit output on a session's first prompt, on any memory change, and every 20 prompts. At the stock caps (48 entries × 256 chars per file) that output can reach about 24.6K characters. Claude Code saves an oversized hook output to a file and puts only a Preview (first 2KB) plus the file path into the model's context. So on those turns the model sees the first several principal entries and none of the DA entries. The 🧠 delta line and the <lifeos-ground> block are written after the memory dump, so they are cut too. Nothing errors, so the loss is silent. It stays invisible until memory fills, which may be why it hasn't been reported: a new install's memory is small.

Where (file:line)

  • LifeOS/install/hooks/MemoryTurnStart.hook.ts:109-111: if (shouldInject(sessionId)) { const memory = loadMemory(); if (memory) process.stdout.write(memory); } runs before the delta (:113-114) and ground (:119-128) writes.
  • LifeOS/install/hooks/LoadMemory.hook.ts:54-71: renders both full files into one block; the caps allow ~12,288 chars each.
  • The header comment in MemoryTurnStart (:42) sizes the block at "~1.5K tokens"; the caps allow roughly four times that.

Repro on a clean tree

  1. Fresh v7.40.4 install. Fill each hot-layer file with 48 entries of about 200 characters each (any content, correct prefixes).
  2. Start a new claude session and send any prompt.
  3. The UserPromptSubmit result for MemoryTurnStart reads Output too large (≈20KB). Full output saved to: … Preview (first 2KB).
  4. Ask the model, with no tools allowed, to quote the last entry of DA_MEMORY.md. It can't; that entry was never in context.

Negative control

Trim both files to about 8 entries each (about 3KB total) and repeat. The output arrives inline, with no "Output too large", and the model can quote the last DA entry. On our install, outputs around 2-3KB always arrived inline, and a 10.1KB output spilled. So the threshold sits somewhere near 10K. We don't know it exactly, and we didn't find it documented.

Suggested fix (shape; tested on a modified install, not on a clean tree)

  • Minimum: write the delta and ground blocks BEFORE the memory dump, so a cut can only take the tail, and warn when the output exceeds a budget instead of failing silently.
  • What we run: @-import both memory files from CLAUDE.md (the same way the identity files load: no size cutoff, prompt-cached). The hook stops dumping; it snapshots entries on the first prompt and then sends only entries added or retired since the last send. If the imports are missing, it falls back to the stock dump.
  • Caveat for that approach: Claude Code loads CLAUDE.md and its imports into custom and general-purpose subagents (only Explore and Plan skip it), so hot memory then reaches every such agent. We moved money and medical detail out of hot memory before switching, and added a curator rule against putting it back.

Related: #2075 (the subagent-detection bug) makes the memory hook skip itself on many installs, which would also hide this one.

  • Searched open and closed issues.
  • Repro run against a clean tree. NOT YET: ours is a modified install; the repro steps above are written to be run on a clean one.
  • Personal data removed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions