Persist the room beside the graph, and write off the actor (#307) - #308
Conversation
|
Gate on e51b80e, private DerivedData: The merged-result failures were real and are fixed by read-through: |
Every graph change re-serialised the whole graph — 323 KB on the graph that filed the issue, 271 KB of it mail bodies that had not changed — and wrote it to disk synchronously inside GraphStore.broadcast(), holding the actor for as long as the disk took: a memo measured at 0.03–2.13 s against a 0.003 s socket round trip. #288's shape, one layer over, and the payload #293 removed from the wire was still in the file. Two fixes, each sufficient on its own. The room lives in its own file (<name>.mailroom.json) written only when its digest changed; the graph file, rewritten on every change, carries no post. A graph saved before the split still loads its inline room. And saves are handed to a GraphWriter — one serial queue, latest snapshot per project wins, so a burst of memos is one write — and the actor returns at once; the daemon flushes it on both exit paths, and tests that read the file straight after a command ask for synchronous persistence. Closes #307. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DeGL2CxuGmq16RSZpJYm2N
…first A load returns the snapshot still queued for a project before falling back to the file, so a delete of a closed project — which reads the graph to find the sessions it must end — cannot see a stale one and leave loops running. Two tests that stood in for a daemon restart inside one process flush the first daemon's writer, as the daemon does on its way out.
e51b80e to
390cdf7
Compare
The defect
GraphStore.broadcast()calledpersistence.saveGraph(graph)synchronously, on the actor, andsaveGraphserialised the whole graph — 323,603 bytes on the graph that filed #307, 271,256 of them (84%) mail bodies that had not changed. So every memo, state tick and cursor move held theGraphStoreactor across a full encode-and-write: anode memomeasured at 0.03–2.13 s against a 0.003 s socket round trip, with the variance coming from the filesystem. It is #288's shape one layer over, and the payload #293 took out of the wire snapshot was still in the file.The change — both halves the issue names, each sufficient on its own
The room lives beside the graph, not in it.
ProjectPersistence.saveGraphwrites the graph without its posts to<name>.json(rewritten on every change) and the room to<name>.mailroom.json, rewritten only when the room'sMailroomDigestchanged since the last write — so a memo rewrites ~52 KB, not 323 KB, and a post rewrites the room once.loadGraphreads both, the sidecar winning; a graph file saved before the split still carries its posts inline and loads exactly as it did.deleteGraphremoves both. The same argument #293 made for the wire, applied to the file.The write leaves the actor.
ProjectRegistryhands each snapshot to aGraphWriter— one serial queue, latest snapshot per project wins — and the store'sonGraphChangedreturns at once; a burst of memos collapses to one write.GraphWriter.flush()waits for everything queued: the daemon calls it on both exit paths (SIGTERM/SIGINT source, the binary-replaced timer) so a change applied a moment before a restart is on disk, andProjectRegistry(persistsSynchronously: true)flushes on every save for the tests that read the file straight after a command.Downgrade note: a daemon older than this reads
<name>.jsononly, so on a downgrade the room would appear empty until the next upgrade — the posts are untouched in the sidecar. Same class as #298, one direction only, and named here so it is not a surprise.Verification
Gate: full Xcode suite (gate on this head in flight; numbers and exit codes in a comment when it completes), swiftlint 0 errors, swift-format clean,
graphcodedandgraphcode-clischemes build; SwiftPMswift buildand.build/debug/graphcodepass.MailroomPersistenceTests: the graph file carries neither amailroomkey nor a post body (a nonce planted in a post never appears in it) while the sidecar does; a change that leaves the room alone leaves the sidecar's mtime alone; loading merges both; deletion removes both; a pre-split graph file with inline posts loads them; a burst of fifty saves lands the newest once afterflush.ProjectRegistryTestsandDuplicateProjectPathTestsrun with synchronous persistence and are otherwise unchanged.Closes #307.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DeGL2CxuGmq16RSZpJYm2N