Repository navigation
Harden the event journal against failed writes, crashes and oversized frames (CF-6, journal mechanics) - #309
Merged
Merged
Conversation
…crash CF-6 (#279), journal mechanics. Two defects the #279 review proved on main: - A write that failed while a client read (resume() flushes) was rethrown to the host or the phone, which answered 500. The batch was gone, onError never ran, and the next resume continued past the lost rows. Now every failed write reports through onError once and marks the journal failed: it records nothing more and resumes no cursor. resume() answers reload instead of throwing. - A process that died inside the 250 ms batch window reopened with the same journal id. A client's pre-crash cursor then resumed past events the state files already held, so a paired desktop kept an answered card looking open. The journal now records `open` in meta and clears it only on a clean close. A journal reopened while still marked open, or one whose write failed, takes a new id, so every earlier cursor reloads. A clean relaunch keeps the id. Tests, written red first: - the read-path failure; - the crash reopen; - a deleted journal's cursor. A crash, a deleted journal and a failed write leave every other file under MODEX_HOME, drafts included, byte-identical. Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), journal mechanics. A paired desktop drops its event stream when a frame passes its 1 MB buffer. With the journal on, it then resumed from the same cursor, got the same frame replayed, and dropped again. It looped until 4 MB of later events piled up, and received nothing after the frame. The review proved this on main with a 1.1 MB item. The client now forgets its cursor before that drop, so the reconnect reloads state, which carries the item another way, and live events continue. The test, written red first, drives the real host and client over TLS. Before the fix, the second connection resumed. Now it reloads once, settles, and receives the next event. Coven-Harness: Claude Code / claude-opus-5-5
…d to end CF-6 (#279), journal mechanics. Both tests use the real host, phone server and desktop client over TLS. - A real Modex process answers an approval. The CLI records the decision and the process is SIGKILLed inside the journal's batch window. After the relaunch: - the journal has a new id; - the paired desktop's and the phone's pre-crash cursors both reload; - the phone's reload shows the card answered; - a counting backend ran nothing, and the CLI heard exactly one decision. - A write that fails while the phone reads stops the journal through index.ts's wiring: - the phone reloads; - the desktop reconnects without the journal and reloads; - live events continue. Both pass with the previous two commits and fail against main's event-journal.ts. Against main, the crash test sees the same journal id, and the phone's read gets 500. Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), unsupported-client fallback. mobileItem had no default case, so a kind the Mac doesn't know went out as undefined, which JSON writes as null. The phone then rejected the whole snapshot (DecodingError.valueNotFound). After a downgrade, the state files can hold such items: today v0.1.0 and v0.2.0 already drop `plan` this way. It now maps to an info notice saying the item comes from a newer Modex. Its own fields are not forwarded, since they may hold anything. The iPhone already shows any notice as plain, inert text. The test was written red first. It uses a made-up kind, so it holds once CF-3 adds a question case. Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279). These run through the real host, phone server and desktop client over TLS. - Unusable cursors reload both clients: - a cursor ahead of the log; - a pruned cursor; - a cursor from a journal whose file was deleted. A control case first shows that a current cursor resumes, so the client really presents the cursor it holds. - About 2.7 MB of missed events replays, each event exactly once. About 4.5 MB reloads instead, and none of it streams. - A journal batch broadcast twice applies once on the paired desktop. These describe existing behavior, so they passed when written. Before this, only unit tests covered these paths. Coven-Harness: Claude Code / claude-opus-5-5
…call counts CF-6 (#279). One run is recorded by a live journal and by one that batches the whole run, so a card's opening and its answer or withdrawal coalesce. Attempt 1: - a commentary message; - a tool that succeeds, then gets a late tool.update; - an approval answered yes, and clicked again; - a second tool; - a final message, then the attempt fails. The retry reuses the native message id. The first attempt's sink then speaks late, and the CLI withdraws a second approval before anyone answers. All of these agree: - the saved files (what a journal-off reload loads); - the runner's projection; - the live stream; - both journals' replays, each also replayed twice; - the phone's mid-run snapshot plus its delta; - a paired desktop that slept through the middle; - a restarted runner. The transcript keeps: - one answered and one withdrawn card; - finished tools that never reopen; - the old attempt's late text in no item. The backend ran twice, the turn and its retry. The CLI heard one "yes", and the withdrawn request settled "no". These describe existing behavior, so they passed when written. Memory-only items are compared through JSON, as the files and clients see them. Coven-Harness: Claude Code / claude-opus-5-5
…is one CF-6 (#279), migration and rollback. The existing migration test wrote its "old" journal with current code. These use the schema older builds actually had, copied from their release tags. - Forward: a file built with v0.1.0's exact schema opens on this build. It has only an events table, with no meta, no identity and no cursors. It keeps every row, gains an identity, replays to the saved transcript, and reloads a client from before cursors. The identity holds across clean relaunches. The draft file and the state files stay byte-identical. - Rollback: a file this build wrote is opened with v0.1.0's statements and then v0.2.0's, verbatim. Both read and append to it, and v0.2.0 keeps the identity and ignores the new `open` flag. Back on this build, the older builds' use doesn't look like a crash. Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), privacy. CF-2 strips route capability tokens from a backend's debug report. This test drives that path through the runner with the journal on. A real ClaudeBackend launches over a fake process with a Pages route whose URL holds a token. The CLI then fails, and Modex stores the failure with its launch report. The failure still names the route, so the report was really stored. The token reaches no event, no journal row and no file under MODEX_HOME, the SQLite file and its WAL included. This passed when written, because CF-2's redaction already holds. Coven-Harness: Claude Code / claude-opus-5-5
…bility CF-6 (#279). docs/event-journal.md now covers: - a write that fails during a read: the journal turns off and clients reload; - the `open` flag, and the new identity after a crash or a lost write; - a desktop reloading after a frame over 1 MB; - the phone's notice for unknown item kinds; - the new fault and convergence tests; - a compatibility and rollback section: the exact tables and meta keys, how a v0.1.0 file migrates, what v0.2.0 and v0.1.0 still do and don't do after a downgrade, and why deleting events.sqlite is always safe. status.md gains a CF-6 journal faults line under "On main, unreleased". The request-half line in event-journal.md is unchanged, since cf6-requests owns it. Coven-Harness: Claude Code / claude-opus-5-5
Brings in CF-3 (#293). One conflict, in mobileItem: CF-3 adds its question case where this branch adds the default for unknown kinds, so both are kept, the question case first. The rollback note now names question alongside plan as a kind v0.1.0 and v0.2.0 send to the phone as null. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
13 tasks
Brings in the CF-6 request half (#308) and #306, #307. One conflict, in mobileItem: #308 rewrote the question case's text, adding the delivery wording, where this branch adds the default for unknown kinds. Both are kept: main's question text, then the default case. Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279). This test was written and run with the rest of this branch, but it was left out of the commits. It passed on the working tree at e973246 (15/15 targeted e2e), while #309's description and the #279 receipt already listed it. With MODEX_EVENT_JOURNAL=1, the app keeps its journal id across a clean relaunch, so paired clients still resume. After its Electron process is SIGKILLed, the next launch takes a new id, so a cursor from before the crash reloads. The test reads `meta` through a read-only handle, so it never marks the journal open or closed itself. Coven-Harness: Claude Code / claude-opus-5-5
Contributor
Author
Correction to the #309 receipt (2026-10-19)The receipt gave #309's head as It is now committed as
The rest of the receipt stands. Val has authorized merging #309 through the guard once its CI passes. |
Brings in #314 (v0.2.0 publication record). Coven-Harness: Claude Code / claude-opus-5-5
Brings in #310 (README and routines spec wording). Coven-Harness: Claude Code / claude-opus-5-5
This was referenced Oct 19, 2026
Brings in #288 (CF-4: completed Codex web searches fold again). Coven-Harness: Claude Code / claude-opus-5-5
BunsDev
added a commit
that referenced
this pull request
Oct 19, 2026
Coven-Harness: Claude Code / claude-opus-5-5
BunsDev
added a commit
that referenced
this pull request
Oct 19, 2026
… and test a card open at a crash (CF-6) (#329) * Record who answered when the phone switches to YOLO or a paired desktop answers (CF-6, finding 9) Switching a thread to YOLO answers the approvals already waiting, and each one was saved as answered by this Mac's window even when the phone set YOLO. Answers and policy changes from a desktop paired through the host were also saved as the window's own. - The phone's policy route passes "phone" to setApprovals, and the runner's YOLO switch answers as whoever made the change. - Commands that arrive through the desktop host carry no IPC event, so thread:answer, thread:answer-question and thread:update record "paired-desktop" for them. - The card says "Answered from a paired desktop." under such an answer. Red first: the companion policy test (by was undefined), the answerNote case, the new runner test (updateThread had no by option) and the Store e2e (no note under the paired desktop's approval). Coven-Harness: Claude Code / claude-opus-5-5 * Test that a card open at a crash reads closed on the phone's first poll (CF-6, finding 3) After a crash the new process closes a card nobody answered only when it first reads that thread. The phone's poll reads its journal cursor before that, so on main at 396b17c the first poll answered delta: true with no items and the card stayed actionable (probe p2 in the #279 review). #309 gives the journal a new id after an unclean shutdown. The phone's old cursor then reloads to a full snapshot, which the same read settles. This test fails on 396b17c ("delta: true, items: 0") and passes with #309's journal. A clean quit, which stops the turn before the journal closes, passes either way and is kept as the control case. Coven-Harness: Claude Code / claude-opus-5-5 * Record the paired-desktop answer and the crash test in status and the journal docs (CF-6) Coven-Harness: Claude Code / claude-opus-5-5
This branch was successfully 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.
Part of #279 (CF-6), under #273. This covers the journal mechanics on
mainthat the #279 review proved defective, plus the fault, convergence, migration and rollback tests #279 asks for. Claim.Not in this PR: the request half of CF-6 (
cf6-requests) stays with its owner and still waits on CF-3 (#293). This PR doesn't touchrunner.ts,human-requests.tsor the iPhone app.Unchanged:
MODEX_EVENT_JOURNALstays opt-in.Not to be merged without Val's go-ahead.
Defects fixed (each test red first)
resume()(the host's/events, the phone's/v1/snapshot) dropped the batch silently, never reachedonError, and later resumes skipped past it (1)onErroronce and marks the journal failed: it records nothing more and resumes no cursor, andresume()answers reloadresumethrewSQLITE_FULL; paired: the phone got 500meta.openis "1" until a clean close; a journal reopened still open, or after a failed write, takes a new idmobileItemhad no default, so an unknown kind (after a downgrade) went out asnulland the phone rejected the whole snapshot (8)undefinedHostile and convergence tests
Paired tests run through the real host, phone server and desktop client over TLS.
Faults:
index.ts's wiring:Unusable cursors reload both clients:
A control case shows that a current cursor resumes.
Replay limit: about 2.7 MB of missed events replays, each exactly once; about 4.5 MB reloads.
Duplicates: a batch delivered twice applies once.
One mixed transcript, every path: the run is recorded by a live journal and by one that batches the whole run.
tool.updateaftertool.done;Migration and rollback:
Privacy:
ClaudeBackendturn fails with a Pages route token in its launch.Drafts: a crash, a deleted journal and a failed write leave every non-journal file byte-identical.
e2e (the app):
Docs
docs/event-journal.mdcovers:eventsunchanged since v0.1.0;metakeysid,pruned_through,open;docs/status.mdgains a CF-6 journal-faults line.Verification
Base
57e7607(main, with CF-3). Heade973246. All 11 commits are signed.Environment: macOS 26.7.1 (25G241), arm64, 16 cores, 48 GB. Node v24.18.1, pnpm 10.34.5, Electron 44.4.5, Playwright 1.63.0, Xcode 26.6, an iOS 26.5 iPhone 16 Pro simulator. The Mac was loaded: load average 25–68, with other agent sessions running.
Unit:
pnpm build && pnpm typecheck && pnpm testonc4565a9, the merge of main. The only change after it is doc wrapping.48eaaf9, before the merge, two tests in files this PR doesn't touch were cancelled by 5–6 s timeouts. Both pass alone (claude-lifecycle25/25,demo3/3).e2e:
playwright testwithevent-journal,companion,desktop-host-launch,store-desktop,workspace-storeandhuman-requests, one973246. 15/15 passed, including the new kill test. The other 69 specs were not run locally; CI runs the full suite.Native: on a dedicated temporary simulator.
xcodebuild … -only-testing:ModexCompanionTests test: 45/45 passed, 0 skipped.Scripts/test-e2e.sh(paired iPhone, real fixture from this tree,MODEX_UI_TIMEOUT_SCALE=2): 43/43 passed. That includes one deliberate expected failure, a simulated screenshot failure.Red-first evidence:
resumethrewSQLITE_FULL; the journal kept its id after a crash; the second connection resumed into the oversized frame;mobileItemreturnedundefined.origin/main'sevent-journal.ts. The crash test failed on "the journal a dead process left open takes a new identity", and the phone-read failure test got500.Not run:
Update after #308 (the CF-6 request half) landed.
mainwas merged in as545c09a.mobileItem. Carry each request's answer, who gave it and its delivery through the journal, replay and the phone (CF-6, request half) #308's newquestiontext is kept, followed by this branch's default case.545c09a: build and typecheck passed. Desktop unit: 1,619 tests, 1,603 passed, 16 skipped, 0 failed. Root suites 109/109 and 16/16; remaining stages 21/21, 10/10 and 16/16.request-journal,human-requestsandhuman-requests-runnerpass alongside this branch's journal rules.Still open (CF-6)
cf6-requests, after Answer each approval and question once, and show Codex questions as cards (CF-3) #293). Review findings 3, 4, 6, 7, 9, 10 and 11:delivery: "sent"before the write;plantonullfor the phone; this fix only applies from this build on.synchronous = NORMAL;