Skip to content

[P1][CF-6] Extend the existing event journal and paired-client replay to preserve typed conversation state #279

Open

Description

@BunsDev

Goal and dependencies

Parent: #273. Priority: P1 — durable recovery. Blocked by: #274 (versioned identity/projection) and #276 (human-request lifecycle). Do not invent a parallel event schema or re-execute tool events.

After reload, sleep, reconnect or restart, recover the same typed transcript and truthful request/outcome state without duplicate actions or lost drafts.

Existing behavior to preserve

The journal guide already documents SQLite/WAL, MODEX_EVENT_JOURNAL=1, journal identity/cursors, batched absolute updates, paired desktop SSE resume, phone delta snapshots and retention. State files remain authoritative. Journal disable/failure falls back to existing reload behavior. This is an extension/migration issue, not a missing-journal implementation.

Preserve defaults unless separately approved: opt-in flag; 250 ms batching; 7-day / 250,000-row retention; 10,000-event replay limit; existing 4 MB replay and 8 MB slow-stream fallbacks. These are the documented baseline values, not new service-level guarantees.

Integration points

Locate the current EventJournal implementation with rg -n 'class EventJournal|replayItems' apps/desktop/src; use that implementation. Existing consumers include apps/desktop/src/main/engine/{store,runner,desktop-host,desktop-client,companion}.ts, apps/desktop/src/renderer/transcript.ts, apps/store-desktop/, and apps/ios/ModexCompanion/.
Extend apps/desktop/test/{event-journal,journal-resume}.test.ts and apps/desktop/e2e/event-journal.spec.ts; add apps/desktop/test/conversation-migration.test.ts. Update docs/event-journal.md and docs/conversation-events.md from #274.

Implementation steps

  • Inventory actual snapshot/journal/client schema versions and create saved legacy/current fixtures. Add richer identity, phase, part, outcome and request-state metadata without promoting the journal to authoritative execution state.
  • Define a consistent snapshot + high-water cursor boundary. Keep coalescing safe for typed fields: do not merge across item/part/attempt/runtime identities, overwrite a terminal state with an old running update, or discard durable request withdrawal. Replay uses idempotent projections, not callbacks that run tools.
  • Extend serializers/validators and the existing desktop/phone merge paths. Negotiate schema capability; unknown future actionable items become non-actionable fallbacks. Preserve historical text and old-client compatible records. Invalid, foreign, expired, ahead-of-log or oversized cursors explicitly reload rather than silently skipping events.
  • Reconcile restart with authoritative native state when available. Pending requests from a dead runtime are expired/unavailable, not active approvals. Unknown execution stays unknown. Never automatically resend a user prompt, command, question answer or approval after an uncertain outcome; explicit user retry creates a new attempt with visible consequences.
  • Keep disk/journal failure behavior safe: existing state/drafts survive, paired streams reset to snapshot mode, and failure of a projection journal does not claim an action was durably audited. Redact sensitive request/credential content before persistence, rather than relying on renderer masking.
  • Exercise migration, batching, wake and paired races with real journal/TLS fixtures, then verify native UI behavior. Record rollback/read compatibility and docs/status.md updates in a signed PR. Default-on journaling is out of scope.

Acceptance criteria

  • Direct projection, snapshot + replay and supported client catch-up converge on identical item identity, text, phase and outcome.
  • Duplicate identified frames are harmless; legitimately repeated deltas remain represented; old-attempt updates do not mutate new attempts.
  • Request withdrawal and resolution survive batching/replay; reconnect never revives a dead card or emits a second native decision.
  • Cursor reset, pruning, journal replacement, out-of-range cursor, replay/stream size limits and journal-off fallback all request the correct reload path.
  • Crash between persistence/broadcast/native send yields truthful unknown/expired state with zero implicit re-execution; old snapshots migrate without text/draft loss.
  • Desktop/Store/iPhone capability mismatches retain readable safe fallback. A legacy binary client cannot approve an unsupported structured question.
  • Existing privacy/retention limits are retained and documented; no raw credentials or sensitive question bodies enter the journal.

Verification: RED → GREEN fixtures, pnpm build, pnpm typecheck, pnpm test, pnpm test:e2e, plus the documented paired journal-resume and supported native iPhone tests. Record exact SHAs, environments and skips rather than claiming all-platform acceptance from one OS.

Long-running goal prompt — implement

Implement CF-6 in TypeSafeAI/modex under #273 using the landed #274/#276 contracts. Read current AGENTS.md, docs/event-journal.md, actual journal/snapshot/client implementations and existing paired tests. Work in an owned worktree with pinned pnpm and Superpowers TDD. Extend the SQLite journal and authoritative state files already present; do not build a new event store, change its default flag or replace the native runtime.
Preserve typed item/part/attempt/generation identity, phases, outcomes and human-request withdrawal across batching, snapshots and desktop/iPhone catch-up. Make snapshot/cursor boundaries coherent and updates idempotent. Treat replay as presentation only. A restart or uncertain native send must not resend prompts, commands or answers; dead requests become unavailable and unknown outcomes remain unknown. Maintain legacy readable fallback, existing retention/size limits, draft ownership and journal-failure recovery. Add hostile migration/cursor/batching/reconnect tests before fixes and count native execution calls to prove no duplicate side effects. Run applicable full and paired suites, update compatibility/rollback docs and status, then prepare a signed linked PR. Leave exact SHAs, commands/results, skips, supported environments and next steps in the checkpoint. Do not enable journaling globally, merge or release.

Follow-up long-running goal prompt — verify and finish

Resume CF-6 from the actual PR/head and checkpoints after verifying worktree ownership. Compare journal-off, direct live projection, snapshot-plus-replay and paired-client catch-up on the same mixed transcript. Inject duplicate frames, an old-attempt update, a cursor from a deleted journal, pruning, future/expired cursors, oversized replay, journal write failure and a crash immediately after an approval write.
Check that request withdrawal survives coalescing, terminal outcomes cannot regress, all sensitive data is filtered before storage, and no executor or native response is called by replay. Load old snapshots and unsupported-client fixtures while preserving drafts. Reproduce defects with failing tests, narrowly repair the existing implementation, and rerun build/typecheck/unit/E2E and paired/native checks. Do not expand into an authoritative event store or switch the feature default. Update docs and the issue receipt with exact schemas, SHAs, results, skips and remaining native gates. Leave incomplete acceptance open; no merge or release without separate authorization.

Activity

  1. BunsDev commented on Oct 18, 2026

    @BunsDev
    ContributorAuthor

    Input for CF-6 from the #273 tracker session, after #282 (CF-1) merged as 7780093.

    Older paired desktops can read unresolved tools as success. CF-1 leaves ok unset on a tool whose outcome is unknown or interrupted (okFor in shared/conversation-events.ts, as docs/conversation-events.md documents). Before 7780093, the runner set ok: false on a tool still open when the turn ended.

    A renderer built before 7780093 reads only ok. It shows ok: undefined as Done, and its fold rule (item.ok !== false) folds the tool away with completed work. A Store client or paired desktop on an older build, connected to a newer host, therefore shows an interrupted or unknown tool as quietly successful. The tracker's done list asks that unsupported clients keep a safe readable fallback. Two options:

    • Send ok: false with those outcomes as the legacy fallback. Current renderers read outcome, so their labels don't change.
    • Or have the paired-client protocol gate the new fields on the client's version.

    I raised this in review on #282 before it merged (review). The contract chose undefined deliberately, so I've left the decision to this issue rather than change the contract myself.

    The iPhone ignores ok and outcome entirely. CompanionViews.swift shows a checkmark for every finished tool (item.status == "running" ? "gearshape.2" : "checkmark.circle"), and companion.ts's mobileItem forwards only status and ok. So failed, denied, interrupted and unknown tools all look done on the phone. This predates CF-1. It's the companion's half of "supported paired-client recovery converge with safe non-actionable fallbacks".

  2. BunsDev commented on Oct 18, 2026

    @BunsDev
    ContributorAuthor

    CF-6 claim: the half that depends only on #274, 2026-10-18, from the session that did CF-1 (#282) and the CF-4 typed-turn slice (#284)

    #276 (CF-3) has not landed; cf3-human-requests is still at 89c98f2. Val asked this session to take only the CF-1-dependent part of this issue now:

    • Convergence. Typed items (origin, phase, reasoning, outcome) converge across direct projection, snapshot plus journal replay, paired-desktop catch-up and phone delta snapshots, with identical identity, text, phase and outcome.
    • Coalescing. Journal coalescing never merges across item identities and never lets an old running update overwrite a terminal state. Duplicate frames stay harmless and repeated deltas stay represented.
    • Migration. Old snapshots and journals keep their text, with no invented provenance, in test/conversation-migration.test.ts. Unsupported clients keep a readable fallback.
    • Docs. docs/event-journal.md and docs/conversation-events.md get updated.

    Left for after #276 lands: request withdrawal and resolution through batching and replay, dead-card revival, the second native decision, and any structured-question capability negotiation.

    • Worktree: .worktrees/cf6-typed-replay, branch cf6-typed-replay, from main at 7780093.
    • Files: event-journal.ts, companion.ts (the phone's item projection), desktop-host.ts/desktop-client.ts only if replay needs it, the journal tests, and the docs.
    • Not touching: the approval and request lifecycle, the runner's request handling, Swift UI.
  3. BunsDev commented on Oct 18, 2026

    @BunsDev
    ContributorAuthor

    CF-6 checkpoint, first half (the part resting on #274): merged in #285 as fb76aac on 2026-10-18. Base 8e26534 after a signed rebase over CF-4; PR head 13a8b33. All 11 checks green, including unit + e2e (macOS) and iPhone companion.

    Done

    • Convergence. Direct projection, snapshot plus journal replay, the live event stream and the phone's full and delta snapshots converge on identical item identity, text, phase and outcome. Tested with a captured Codex 0.163.0 turn through the real adapter, runner and journal.
    • Duplicates and attempts. Duplicate frames are harmless: replayItems now upserts by id, where before it appended a stale copy. Repeated deltas stay represented, and a retry reusing a native id never merges into the first attempt.
    • Crash or restart. Work a dead process left running becomes unknown in the saved snapshot, the journal and every client, not only in one window, and nothing runs again. Before, clients resuming from the journal kept a spinner forever.
    • Old history. Old snapshots and journals load, replay and project unchanged, with no invented provenance (test/conversation-migration.test.ts).
    • The phone. It receives phase, outcome and reasoning. Today's build, which reads only title and status, sees a failed, denied, stopped or unknown tool in its title.
    • Unchanged: privacy and retention limits; the typed fields travel in existing events as whole values.

    Checks at 01a395b, before the rebase

    • pnpm test: the first stage ran 1,520 tests, with 1,504 passed, 0 failed and 16 skipped. The second stage passed 21 of 21.
    • Focused e2e passed 34 of 34, covering event-journal, companion, store-desktop, store-demo, desktop-host-launch, app, sources and agents.
    • After the rebase: typecheck passed, and the affected suites passed 127 of 127.
    • Red first: the duplicate-frames, convergence (phone) and restart tests each failed before their fix.

    Still open, after #276 (CF-3) lands

    • Request withdrawal and resolution through batching and replay.
    • Reconnect never reviving a dead card or emitting a second native decision.
    • Structured-question capability negotiation, and a legacy client unable to approve an unsupported question.
    • Approval and question privacy in the journal.
  4. 23 remaining items

  5. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    Correction to the #309 receipt (2026-10-19)

    The receipt gave #309's head as e973246 and listed an e2e/event-journal.spec.ts test: the app keeps its journal id across a clean relaunch and takes a new one after a SIGKILL. That test was written, and it passed in the 15/15 targeted e2e run on the working tree, but it was never committed. So e973246, and the 545c09a head that followed it, did not contain it, and CI on those heads never ran it.

    It is now committed as 0e2964e, on top of a merge of main at b804e9c, which brings in #312 and #313. The current state:

    The rest of the receipt stands. Val has authorized merging #309 through the guard once its CI passes.

  6. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    CF-6 review findings re-checked against main at 396b17c (2026-10-19)

    From the docs steward session. Each state below is backed by a test or probe from this session.

    # Finding State Evidence
    3 A card left open at a crash stays actionable Reproduced, and fixed by #309 New test/request-crash.test.ts. On 396b17c the phone's first poll after a crash returns delta: true with 0 items, so the card stays open. With #309's journal, which takes a new id after an unclean shutdown, the old cursor reloads and the card reads closed. A clean quit passes either way (control case). Committed on cf6-residue as 2201eca, to push once #309 is on main.
    4 delivery: "sent" recorded before the write Owned by #316 #316 records sent only after the backend's write callback.
    6 Older paired desktops render nothing for a question Confirmed by reading; needs a decision (below) A paired desktop sends only protocol: 1 and a fixed app name (desktop-client.ts:128, 146, 226), never its version, so the host can't tell a pre-CF-3 Store build from a current one.
    7 iPhone shows a withdrawn approval as "Denied" Owned by #316, fixed on the Mac's side #316's mobileItem sends withdrawn, sending and uncertain approvals as notices, which today's iPhone build already renders.
    9 Answers credited to the wrong client Fixed on cf6-residue (a498ca7), PR to follow Red first: the companion policy test (by undefined), answerNote, a new runner test and the Store e2e (no note under the paired desktop's approval). Green: 25/25 unit tests, and 15/15 e2e across store-desktop, human-requests, approvals and approval-policy.
    10 A failed save at accept time strands the request Reproduced on 396b17c Probe, with saveItems throwing once: {"first":"threw ENOSPC: no space left on device","retry":{"ok":false,"reason":"already-resolved"},"cliHeard":[],"status":"waiting","card":{"answer":"yes","delivery":"sending","withdrawn":null}}.
    11 The phone doesn't refresh after a 409 Fixed on cf6-phone-refresh, PR to follow Two new XCTests failed first (1 snapshot request where 2 were expected). After the fix, ModexCompanionTests passes 47/47 on an iPhone 16 Pro simulator.

    Finding 10 and #316. #316 rewrites exactly this part of answer(). It now keeps the pending entry until delivery, but a failed save still leaves the request resolving until Stop, which then turns it uncertain. One narrow fix: if the save fails, put the broker back to pending and refuse the answer, so "saved before the CLI hears anything" still holds and the person can retry. To #316's owner: say here whether you'd rather fold that into #316. Otherwise I'll take it once #316 lands, so the two branches don't edit answer() in parallel.

    Question for Val (finding 6). Store builds cut before CF-3, including TestFlight Store 0.0.8, show nothing for a Codex question. That's safe, because their answers are refused, but the thread just appears to wait. Options:

    • A. Bump DESKTOP_PROTOCOL to 2. Older Store builds show the existing "The host desktop protocol is incompatible. Update the host and Store app" screen, which keeps saved access and drafts, until they update. Small change. They stop working until updated.
    • B. Add a capability field. New Store builds send one when they connect. The host sends question items, and any later kind, as a readable notice to clients without it, as it already does for the phone. Old builds keep working. This needs per-client mapping in both journal replay and the live broadcast, so it's more code.
    • C. Leave it as is. Safe, but not readable.

    I'd pick A, since the Store app is still a preview. I won't change the protocol until you choose.

    Nothing was merged, tagged or released. #309 and #315 merge through the guard after #299, as posted on #273.

    Coven-Harness: Claude Code / claude-opus-5-5

  7. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    CF-6 residue: where each review finding stands (2026-10-19)

    #309 landed at 17:55Z as c345779, with all 11 checks green on b18a90a. Its squash tree matches the branch. That covers review findings 1, 2, 5 and 8.

    # Finding State Where
    3 A card open at a crash stayed actionable Fixed by #309; regression test in review #329: test/request-crash.test.ts fails on 396b17c (delta: true, items: 0) and passes on c345779
    4 sent recorded before the write Owned by #316 —
    6 Older paired desktops show nothing for a question Fixed in review #330. Val chose a capability field over a protocol bump, because a bump would leave TestFlight Store 0.0.8, which predates #166's recovery screen, retrying "Waiting for your Mac host" indefinitely. Clients now name the item kinds they draw in x-modex-item-kinds, and a client that names none gets a question or a plan as a notice. Red first in c44985f.
    7 iPhone shows a withdrawn approval as "Denied" Owned by #316, on the Mac's side —
    9 Answers credited to the wrong client Fixed in review #329
    10 A failed save at accept time strands the request Reproduced; waiting on #316 No reply yet from #316's owner on folding it in
    11 The phone doesn't refresh after a 409 Fixed in review #318

    Checks on each head (pnpm 10.34.5, Node 24.18.1):

    All three are code PRs, so they wait for Val's go-ahead to merge.

    To #316's owner: #316 now conflicts with main in companion.ts. #309 added a fallback for unknown item kinds to mobileItem, right after the question case, where #316 rewrites the approval case. Its status.md and human-requests.md changes still merge cleanly with #329, #330 and #331.

    Still open for #279: finding 10, answering questions from the phone, and the native gates: live providers, a physical iPhone, Windows and Linux, the 8 MB slow-stream drop, and power loss. None of #318, #329 or #330 has run against a live provider, the installed TestFlight Store 0.0.8 or a physical iPhone.

    Coven-Harness: Claude Code / claude-opus-5-5

  8. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    #329 CI update. On 30ac266, unit + e2e (macOS) was cancelled at the 45-minute limit. The unit-test step went silent after attachment-recovery-file at 18:14Z and stayed silent until the cancel.

    So #318, #329 and #330 are all green on their heads. #318 and #330 are behind main and get it merged in on Val's go-ahead.

    Coven-Harness: Claude Code / claude-opus-5-5

  9. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    Val gave the go-ahead to land the three CF-6 repair PRs. I'm landing them one at a time through the merge guard:

    main requires branches to be up to date, so a landing in between sends the next PR back through CI. If you can hold other merges until #318 lands, please do.

  10. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    All three CF-6 repair PRs have landed through the merge guard, on Val's go-ahead. Each squash commit has the same tree as its branch head.

    Finding PR On main
    3 (a card left open at a crash) Fixed by #309; regression test in #329 9a2e15f
    9 (answers credited to the wrong client) #329 9a2e15f
    6 (older paired desktops show nothing for a question or plan) #330 9c0b063
    11 (the phone doesn't refresh after a 409) #318 de856e0

    On #330, CodeQL flagged one high-severity alert. The cause was the test's raw legacy client, which turned certificate validation off. 71bae66 makes it pin the host certificate the same way DesktopClient does. #317 landed during that CI run, so #330 needed one more.

    Still open on this issue:

    #316 conflicts with main, and its Codex thread last wrote at 2026-10-19 03:55Z. I've asked Val on #290 whether it's still live.

    Not run: a live provider, an installed TestFlight Store 0.0.8, or a physical iPhone. #318 reaches phones only in the next Companion build. The acceptance boxes above stay unticked until those gates run.

  11. BunsDev commented on Oct 20, 2026

    @BunsDev
    ContributorAuthor

    At Val's request, finding 10 (a failed save at accept time strands the request) is now in its own PR: #336, on a fresh branch from main.

    answer() and answerQuestion() now save the card before deleting the pending entry. If the save throws, the broker state and the card go back to what they were and the error goes to the caller, so the person can answer again. This is the same rule addItem already follows. Two runner tests failed first on de856e0, one for an approval and one for a question; both pass now. A third test pins that a failed write that still reached the disk reads as uncertain after a crash.

    Local results:

    • build, typecheck and root pnpm test pass: desktop 1,664 passed, 16 skipped, 0 failed;
    • the answer-path e2e passes 17/17 (human-requests, approvals, approval-policy, companion and store-desktop).

    To #316's owner: #316 rewrites the same part of answer(). #336 changes only the order (save, then delete the pending entry) and adds a rollback if the save fails, so it should carry over to whichever branch lands second.

    #336 is a code PR, so it waits for Val's go-ahead to merge.

  12. BunsDev commented on Oct 20, 2026

    @BunsDev
    ContributorAuthor

    Every CF-6 review finding has now landed on main.

    Finding Landed in On main
    3 #309, regression test #329 c345779, 9a2e15f
    4 ("sent" before the write) #335 (lands #316) 0f5c785
    6 #330 9c0b063
    7 (the iPhone shows a withdrawn approval as "Denied") #335 (lands #316) 0f5c785
    9 #329 9a2e15f
    10 (a failed save strands the request) #336 1b6f7a43
    11 #318 de856e0

    On finding 10:

    On finding 7:

    Still open on this issue:

    • answering questions from the phone;
    • the native and live gates: live providers, a physical iPhone, Windows and Linux, the 8 MB slow-stream drop, and power loss.

    None of the repairs has run against a live provider, the installed TestFlight Store 0.0.8 or a physical iPhone. The acceptance boxes above stay unticked until those gates run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions