Skip to content

fix(session-start): stop re-injecting the whole recap at source=compact - #340

Merged
fdaviddpt merged 2 commits into
mainfrom
fix/339
Aug 10, 2026
Merged

fix(session-start): stop re-injecting the whole recap at source=compact#340
fdaviddpt merged 2 commits into
mainfrom
fix/339

Conversation

@fdaviddpt

Copy link
Copy Markdown
Contributor

Closes #339

The reported problem, and the part of it that was asserted

session-start-hook.sh reads HOOK_STDIN for session_id and discards source, so the full memory recap is printed identically at startup, resume, clear, compact and fork. The reporter measured it across 50 sessions: compact fired 82 times against startup's 180, at comparable per-fire cost.

That measurement is of cost, not of overlap. The issue's stated rationale — that at compact the conversation already holds a summary covering the same ground — was asserted rather than measured, and this repo's own README says close to the opposite: auto-compact discards conversation history before the save pipeline can capture it, which is why the README recommends turning it off. A compaction summary is task-focused and lossy. So "skip the recap at compact" would trade a measured ~459 chars for an unmeasured, silent loss of identity and long-term memory for the rest of every long session — an absence read as a presence, which is the defect class this repo files most often.

What this does instead

The .remember/ pointer is not at stake. === REMEMBER === is emitted unconditionally further up and already names now.md, today-*.md, recent.md, archive.md and core-memories.md. The one file it does not name is identity.md, and that asymmetry is what the fix is built on:

At source=compact Treatment Why
identity.md injected in full presence-required — a path to it does not make the agent behave as that persona, and no other line of output names the file
core-memories.md, today-*.md, now.md, recent.md, archive.md named, with byte sizes recall-on-demand and already addressable

Named rather than dropped, for #124's reason: a recap that shrinks in silence is indistinguishable from a store that emptied.

startup, resume and clear are unchanged. fork is unchanged deliberately — what a fork inherits from its parent context could not be established, and an unverified belief is not grounds for withholding memory.

Absence must not read as compact

source is extracted by the same narrow shell heuristic as session_id (_stdin_session_id is now a thin wrapper over a shared _stdin_json_string KEY RAW), then whitelisted against startup|resume|clear|compact|fork. Anything else — missing field, empty value, unknown spelling, no stdin at all — becomes "" and takes the unchanged full-recap path. A payload shape we do not recognise must never silently disable memory injection.

post-tool-hook.sh keeps its own copy of the heuristic. It reads one field, and sourcing a library from a hook that has to survive a broken install is the worse trade.

The memory file list existed twice and now needs a third read, so it is hoisted into a MEMORY_FILES array — a seventh file cannot be added to one loop and missed by the others.

Tests

Written first. The first RED run produced only one failure, because a full recap already prints --- core-memories.md --- as a body header, so the basename assertion passed against unchanged code — a test that would pass if the code did nothing. Rewritten to assert the resolved path plus the byte size:

>       assert BODIES[name] not in out, name + " was re-injected at source=compact"
E       AssertionError: core-memories.md was re-injected at source=compact
>       assert str(remember / name) in out, name + " was withheld without a path"
E       AssertionError: core-memories.md was withheld without a path
>       assert str(size) + " bytes" in out
E       AssertionError: assert ('16' + ' bytes') in '=== LAST HANDOFF ===\n...'

3 failed, 15 passed in 12.05s

After the fix: 18 passed in 10.40s. Full suite: 1569 passed, 43 skipped in 879.20s. Windows unverified locally; CI is the authority there.

Review

One independent Sonnet agent against the committed diff, with no sight of the author's reasoning — it re-derived the RED behaviour by running the test file against HEAD~1 in a scratch clone. Two findings, both fixed: #206's comment still claimed only session_id was needed from stdin, and the fixture covered five of six MEMORY_FILES (today-<date>.md was never written). Nothing refused.

Found in passing, not fixed here

  • === LAST HANDOFF === increments its delivery counter on every compaction, because the fingerprint is unchanged and the "already delivered" branch fires. Four compactions render as "already delivered 5 times" for a note delivered once.
  • The consolidation trigger re-fires on every compact when past-day staging files exist.

Both are pre-existing and unrelated to source. Narrowing either one here would be exactly the silent scope change this change is trying not to make.

…ct (#339)

SessionStart fires again at every auto-compaction. The hook read session_id
out of that payload and discarded `source`, so a compaction got the same full
recap a cold start does.

A compaction is not a new session: the store has not changed and the bytes
were already delivered once, in this session. So at source=compact the bodies
are not repeated -- except identity.md, which works by presence rather than by
pointer and which nothing else in the output even names. The rest are named
with their sizes (#124's "kept but not injected"), because a recap that
shrinks in silence is indistinguishable from a store that emptied.

Read in one direction only. No `source`, an empty value, an unknown spelling
or no stdin at all leaves it unrecognised and takes today's path unchanged.
startup/resume/clear/fork, the handoff blocks, the history hint, the
consolidation trigger and the hooks.d dispatches are untouched at every
source; neither the recovery block nor the capture-gap check branches on it.

Co-Authored-By: Max <noreply>
…339)

The fixture wrote five of the six MEMORY_FILES entries; today-<date>.md was
never created, so nothing exercised the largest deferred file.

The #206 comment above the stdin read still said only session_id is taken out
of the payload. True for the job that comment is about, and no longer true of
the block as a whole.

Co-Authored-By: Max <noreply>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant