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
- Fresh v7.40.4 install. Fill each hot-layer file with 48 entries of about 200 characters each (any content, correct prefixes).
- Start a new
claude session and send any prompt.
- The UserPromptSubmit result for MemoryTurnStart reads
Output too large (≈20KB). Full output saved to: … Preview (first 2KB).
- 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.
Version
LifeOS v7.40.4 (latest release). Claude Code 2.1.268.
What is broken
MemoryTurnStartwrites 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 aPreview (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.MemoryTurnStart(:42) sizes the block at "~1.5K tokens"; the caps allow roughly four times that.Repro on a clean tree
claudesession and send any prompt.Output too large (≈20KB). Full output saved to: … Preview (first 2KB).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)
@-import both memory files fromCLAUDE.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.CLAUDE.mdand 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.