Repository navigation
Conversation
- Re-read memory.md as raw bytes right before the rename and compare it with the condensed snapshot. - Carry an agent's appended tail over verbatim, re-checking up to three rounds. - Abort on any other change with source-changed, and on a missing or unreadable file with source-check-failed. - Refuse a non-UTF-8 source before the summary call with source-encoding-invalid. - Remove the temp file on every failure path and log carriedBytes on success.
- Gate the summarizer so tests can mutate memory.md while the call is pending. - Cover carried appends, multi-byte boundaries, in-place edits, deletion and rename failures. - Assert the temp file is cleaned up and the carried tail survives the next pass.
- Remove the literal 0 that HarnessMD#508 left in front of the doc comment. - Keep behavior unchanged, since ASI already made it a no-op statement.
aaroncoville
added a commit
to aaroncoville/munder-difflin
that referenced
this pull request
Nov 12, 2026
Condensing reads memory.md, waits on a summary call that can take minutes, then renames the rebuilt file over the original. Anything an agent appended in between was overwritten by the rename and lost. The swap now re-reads memory.md as raw bytes just before the rename and compares it with the snapshot the summary was built from: - bytes appended after the snapshot are carried onto the new file verbatim, re-checking up to three rounds while the file keeps growing; - any other change aborts with source-changed (the next pass retries from the new file), and a missing or unreadable file aborts with source-check-failed; - a source that is not valid UTF-8 is refused before the summary call (source-encoding-invalid), since a lossy decode would corrupt it; - the temp file is removed on every failure path, and a successful swap logs carriedBytes. The not-smaller back-off on the autonomous loop is unchanged. Ported from upstream PR HarnessMD#842 (commits 9a560db, 6143a40, ef78130). Co-authored-by: Aaron Coville <aaron@woodenbird.io>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
When the reflector condenses an agent's
memory.md, it reads the file, waits on the headless summary call (up to 180 s), rebuilds the file from what it read, and renames the result over the original. It never looks at the file again before the rename, so anything an agent appends during that wait gets silently overwritten. There's no error, nocondense-abortline, and the lost text isn't inhive/backups/either, since the backup is taken before the wait. Agents are told to append tomemory.mdas they work, and the reflector runs on a timer with no idle check, so this can happen in normal use.This PR makes the swap check the file one more time right before the rename. If the file still starts with exactly the bytes that were condensed, any new bytes after them (an agent's append) get copied onto the end of the rebuilt file and kept. If anything else changed, the pass aborts with
source-changed, the agent's file is left alone, and the next tick tries again from the new contents. Onlysrc/main/reflect.tschanges, plus a new test file.Where this fits. I'm planning a few small PRs for the condenser, and this is step 1. It only covers the input side: never swap over a file that changed while we were waiting on the summary. It doesn't try to fix everything in #501 or #744 on its own. Roughly in order:
memory.md.memory-archive-rawbefore I start on this one.Each one is its own PR to keep reviews small, and this PR is useful on its own without the others.
Refs #501, Refs #744
condensecondensewithcarriedBytescondense-abort reason=source-changedsource-changed, detailstill being appendedsource-check-failedswap-failed, same as todayA few details:
source-encoding-invalid) instead of being rewritten through a lossy decode.carriedBytes) and a few newreasonvalues. Memory format, agent protocol, IPC, and config are unchanged.Known limit: this is an optimistic check, not a cross-process compare-and-swap. The last re-read and the rename run back to back with no
awaitbetween them, so the window shrinks from the whole LLM call to a few milliseconds, but a write that lands in exactly that gap can still be lost. On POSIX, a writer that opened its fd before the rename keeps writing to the old inode. Closing that gap completely would need a single writer per file, which felt like a bigger design call than this PR should make.Type of change
Evidence
Before
On
main, the new tests go red. 14 of the 17 fail, and the first one is the bug itself: an append made while the summarizer is pending doesn't survive the condense.I also ran a stress test on Windows (mock LLM, real NTFS, 300 condense passes, with a second process appending every 20 ms, which is far more often than a real agent writes). On
main, every append made during the LLM wait was lost:swap-failed(EPERM)mainAfter
Same 17 tests on this branch: all green.
Same stress test on this branch:
swap-failed(EPERM)The remaining external-line loss is the few-millisecond window from the known limit above; none of it happened in aborted passes. Every
swap-failedleft the file untouched with the temp file removed, and the next tick retried.And in the real dev app with a real hidden Claude run. I seeded the god agent's
memory.mdwith 30 sections, triggeredawait window.cth.reflectNow('god')from DevTools, and had a script change the file while the summary call was pending:condensewithcarriedBytes: 418(11681 to 3992 bytes). All 11 markers are still in the file afterwards (3992 + 418 = 4410 bytes), and no temp file was left behind.condense-abort reason=source-changed.memory.mdkept the edit (11543 bytes, not overwritten), and no temp file was left behind.How I tested it
node --test test/reflect-*.test.cjs: 24/24 pass (17 new plus the 7 existingreflect-summarytests). The new file fails 14/17 onmain, and 7/17 against an abort-only version of the fix, so the tests cover the carry-over path too, not just the abort.npm run typecheck(node and web): pass.npm run test:focusedon this branch and onmain9bc1e5a: 944 pass / 24 fail vs. 927 pass / 24 fail. The extra 17 passes are the new tests. The 24 failures are the same list on both sides, all Windows-only symlink, worktree, and electron-builder tests that have nothing to do with reflect.npm run build: succeeds.summarize-failedbecause the hidden run never wrote a transcript, so the swap never ran. A retry went through fine. That failure is on the [BUG] Munder Difflin v0.5.2 — condense subsystem kills the app during idle compaction + corrupts agent notebooks #501 Bug 2 path (a hidden run that writes nothing), which step 3 will cover; this change doesn't cause it.The second commit is the new test file. The third removes a stray
0before the summary-marker doc comment that I left behind in #508 (it parsed as a harmless0;statement).Heads up: #671 also touches
reflect.ts. I checked a merge against its current head and it goes through without conflicts.Checklist
npm run typecheckpasses.npm run test:focusedpasses. (No new failures: the same 24 Windows-only failures show up onmain.)npm run buildsucceeds.commented-out code, or unrelated formatting churn in it.
DESIGN.md/tokens.ts— no ad-hoc colors,spacing, or fonts. (No UI changes.)
ATTRIBUTION.md. (No art added.)