Skip to content

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

Closed

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

    CF-6 second half: read-only review of cf6-requests at 21bb6cb

    From the docs steward (#290), at Val's request.

    • Scope: a review only. I didn't take the branch over.
    • Code under review: 21bb6cb, the five commits on CF-3's 173a17a. origin/cf6-requests is still at 01df9b8, an earlier copy of the same five commits, so the reviewed head hasn't been pushed.
    • Method: I exported a copy of the tree and built it with pnpm. The probes ran against that copy. Nothing in the repository or the claimed worktree changed. Line numbers are in the 21bb6cb tree.
    • Labels: every finding marked proven has a probe I reran myself, with its output below. By reading means I read the code but didn't run anything.

    Baseline at 21bb6cb

    The branch's own suites pass:

    • request-journal: 6/6
    • human-requests: 7/7
    • human-requests-runner: 6/6
    • companion: 15/15
    • event-journal: 11/11
    • journal-resume: 2/2
    • conversation-migration: 4/4

    What holds

    • Nothing is sent twice. No replay, reconnect or restart path resends a prompt, command or answer.
      • replayItems is pure (event-journal.ts:220-236).
      • Paired desktops only apply item events (App.tsx:398-400).
      • The phone snapshot is read-only (companion.ts:257-279).
      • Native sends happen only through runner.answer/answerQuestion.
    • One-batch coalescing replays correctly (proven). A whole lifecycle in a single batch replays identically to the runner's cards, even replayed twice. That lifecycle: a question Codex withdraws itself, an answer plus a duplicate answer, a withdrawal, and a Stop. Codex received exactly one answer and one stop error. merge only lays later patches over earlier ones (event-journal.ts:206-211).
    • Secret answers stay out of every file. They're redacted before saving (runner.ts:959). The branch's test scans every file under MODEX_HOME, including the SQLite file and its WAL, plus events and the phone view.
    • A legacy client can't answer a structured question (proven).
      • A legacy phone approval on a question id gets 409.
      • answers {} from the phone gets 400.
      • A paired desktop's thread:answer on a question gets {ok:false, reason:"invalid"}.
      • Codex receives nothing. The phone sees the question as a read-only notice (companion.ts:401).
    • Unchanged since main 7ec5f14: event-journal.ts, desktop-host.ts, desktop-client.ts and desktop-protocol.ts. So the opt-in flag, 250 ms batching, 7-day / 250,000-row retention, 10,000-event replay, 4 MB replay and 8 MB stream limits are all as before.
    • Two cursor cases reload correctly (proven through the real host). An ahead-of-log cursor reloads. A replay of 3.9 MB resumes, and one of 4.3 MB reloads.

    Findings, most severe first

    1. High, already on main: a failed write during a read silently drops a batch, and the journal stays on (proven, probe c1).

    • Where: resume() → latest() → flush() (event-journal.ts:146, 155). flush() empties the batch before writing (:106-107) and rethrows. Both the host's /events and the phone's /v1/snapshot call resume(), and they only answer 500 (desktop-host.ts:77, 237; companion.ts:150, 264). stopJournal only runs for timed flushes (index.ts:173-181).
    • Output (insert made to throw SQLITE_FULL once): {"thrownFromResume":"Error: SQLITE_FULL…","onErrorCalls":0,"closed":false,"resumeAfter":{"reload":false,"latest":2,"seqs":[2],"ids":["c"]}}. The failed batch, event b, is gone, and the next resume continues past it without reloading.
    • Docs: this contradicts docs/event-journal.md:15-16, which says a write failure turns the journal off.
    • Fix: send every flush failure through onError, or mark the journal failed so resume() always answers reload.

    2. Medium: a crash inside the 250 ms batch window keeps the journal's id, so a resumed client never sees an answer that was saved (proven, probes c5 and A3).

    • Where: the runner saves the thread file and then emits (runner.ts:1305-1324). The batch is in memory only (event-journal.ts:48, 95-99), and the reopened file keeps its id (:74-75).
    • Output (c5: real runner, card answered, then the process was killed with SIGKILL inside the window): {"childSignal":"SIGKILL","savedCard":{"answer":"yes","answeredBy":"desktop","delivery":"sent"},"resume":{"sameId":true,"reload":false,"entriesAfterCursor":[]},"pairedDesktopCard":{"answer":null,"delivery":null}}.
    • What the user sees: the stale click is refused as already-resolved, and the new process emits nothing (A3). The renderer drops {ok:false} silently (App.tsx:722), so Approve and Deny just do nothing, indefinitely. The CLI heard exactly one decision.
    • Fix: record a clean or open flag in meta. After an unclean shutdown, give the journal a new id, or a per-boot id in the connected frame, so every earlier cursor reloads.

    3. Medium: cleanup after a restart is lazy, so a card left open at a crash stays actionable on paired desktops (proven, probes p3/A2 and p2).

    • Where: cards are closed as ended only when a thread first loads in the new process (runner.ts:207-241). Startup deliberately doesn't load transcripts.
    • Paired desktop (p3): 1.5 s after resume, paired desktop card: {"answer":null,"withdrawn":null}. Then click Approve -> {"ok":false,"reason":"withdrawn"}, and only after that click does the card close.
    • Phone (p2): the endpoint reads its cursor (companion.ts:264) before items() runs the cleanup (:266). So the first poll returns changedItems=0, and Approve/Deny stays for one 2.5 s poll.
    • Status: at 21bb6cb, status.md says "cards a restarted Modex left open close as ended everywhere", which this contradicts.
    • Fix: close open cards across all stored threads at runner startup, before the host or companion serves anything. For the phone alone, it would be enough to call items() before resume().

    4. Medium: delivery: "sent" is saved before the backend writes anything, and stays "sent" when the write never happens (proven, probe A4).

    • Where: answer() calls settle() and then sent() in the same tick (runner.ts:939-940, 966-970). The backend's stdin write comes later, and does nothing if stdin is gone (codex.ts:125-127, claude.ts:283-291).
    • Output (real CodexBackend, app-server stdin destroyed before the answer): {"answers":{"q-env":["staging"]},"delivery":"sent"}, on disk sent, replies the CLI received: 0.
    • Consequence: uncertain appears only after a kill between two back-to-back saves (runner.ts:222). The human-requests.md rows for "handed to the CLI" and uncertain overstate what is recorded.
    • Fix: confirm delivery after the write callback, and mark the card uncertain when the write is skipped or fails.

    5. Medium, already on main: one event over 1 MB traps a paired desktop in an endless "resumed" reconnect loop (proven, probe c3).

    • Where: the client destroys the stream above 1 MB (desktop-client.ts:276), then resumes from the same cursor into the same frame.
    • Output (journal on): {"connectedEvents":12,"resumedFlags":[false,true,true,…],"received":["small"],"state":"offline"}. With the journal off, the same drop reconnects once with resumed:false and reloads.
    • Reachable in practice: the Mac accepts a 1 MiB message (runner.ts:572), and escaped tool output grows past 1 MB.
    • Fix: before retrying after an oversized frame, drop the cursor, or have the host replace the frame with a reload.

    6. Medium: there's no capability negotiation, and older paired desktops show nothing for a question.

    • DESKTOP_PROTOCOL is still 1 (desktop-protocol.ts:1), so Store builds cut before CF-3 connect normally. That includes TestFlight Store 0.0.8.
    • Their renderer's item switch has no question case (git show 6db0818:apps/desktop/src/renderer/components/ThreadView.tsx, lines 228-253; cases are user, assistant, tool, approval, notice, thinking, route, plan).
    • So a blocking Codex question renders no card and the thread just waits. It's safe, because an answer is refused, but not readable.
    • Fix: bump the protocol, or send a capability field and map question to a notice, as companion.ts:401 does for the phone.

    7. Low–medium: the in-tree iPhone app shows withdrawn approvals as "Denied" (by reading).

    • CompanionItem decodes neither withdrawn, delivery nor answeredBy (CompanionAPI.swift:87-100), and any answer:"no" renders "Denied" (CompanionViews.swift:752-753).
    • human-requests.md:190-192 says so. status.md ("the phone carry it") and ios-companion.md don't.

    8. Low–medium: downgrading breaks the phone, and nothing documents it (proven, probe c6 plus the release tags).

    9. Low: answers are attributed to the wrong client.

    • A YOLO switch answers pending approvals with answeredBy:"desktop", even when the phone set YOLO (runner.ts:1058, companion.ts:326; proven, {"answer":"yes","answeredBy":"desktop","decidedBy":null}).
    • Answers from a paired desktop are recorded the same as the host window's (index.ts:820-821, commands.ts:99-100, desktop-host.ts:258 pass no by or grant).

    10. Low (by reading): a failed save at accept time strands the request. patchItem saves before emitting (runner.ts:1316-1323). By then answer() has removed the pending entry and moved the broker to resolving (:933-937). So the CLI never gets the answer, the card stays open, and retries are refused until Stop.

    11. Low (by reading): the phone doesn't refresh after a 409. CompanionModel.answer sets only error (CompanionModel.swift:386-392), under a "Connection issue" alert (CompanionViews.swift:64). The card stays actionable until the next poll.

    12. Low (docs): some request text is stored and sent verbatim. Approval command text and detail, and question prompts, including a secret question's prompt, go into the journal and out to clients unredacted (companion.ts:391, 401). This predates CF-6, but the Secrets section of human-requests.md:196-201 should say so.

    Against the goal's test requirements

    • Native writes are counted in only one test, the secret test (request-journal.test.ts:247). The others count backend callbacks (heard) or runTurn calls.
    • The "simultaneous answers" test isn't a race (proven, probe-race). The desktop answers before the phone's request arrives, so the companion's pre-check returns 409 and the broker is never reached. In 20 runs the result was {"desktopWins":20,"phoneWins":0,"phoneReachedRunner":0}.
    • No test kills a process. Both crash tests edit saved state by hand (request-journal.test.ts:182-187 and the e2e spec).
    • No request test coalesces into one batch. The harness flushes every 5 ms (:42).
    • Red-first isn't visible in the history. The tests landed in the same commit as the code (2268ac6).

    #279's deliverables at 21bb6cb

    Deliverable State
    test/event-journal.test.ts Extended by #285 on main, not by this branch
    test/journal-resume.test.ts Unchanged since #253
    e2e/event-journal.spec.ts Unchanged since #206. A journal-on case went into e2e/human-requests.spec.ts instead
    test/conversation-migration.test.ts Present (#285, #301). Its "old" journal is written by current code (:46-47)
    docs/event-journal.md Updated (:127-130). :15-16 is contradicted by finding 1
    docs/conversation-events.md Only #285's paragraph
    Rollback / read compatibility Missing (finding 8)
    docs/status.md It drops CF-6 from "Still open" (:110-111), and "close as ended everywhere" is overstated (finding 3)

    Tests that would close these

    1. Write failure during resume(): the journal turns off or reloads, and no batch is lost (finding 1).
    2. Answer, then SIGKILL inside the batch window, then restart. A paired desktop and the phone resume with old cursors and must reload or see the card answered (finding 2).
    3. A card open at a crash, then restart without loading the thread. A resumed desktop, and the phone's first delta, must show it ended (finding 3).
    4. A CLI whose stdin is closed when the answer is accepted. The card must not say "sent", and native writes must be counted as 0 (finding 4).
    5. A frame over 1 MB through the real host and client must reload, not loop (finding 5). Also the 4 MB replay and 8 MB slow-stream limits through the host, neither of which has a test today.
    6. A protocol-1 paired desktop or legacy renderer meeting a question item (finding 6). An iPhone test that a withdrawn approval isn't labelled "Denied" and that the app refreshes after a 409 (findings 7 and 11).
    7. Rollback fixtures: an older build reading this build's state files and journal (finding 8).
    8. A real desktop/phone race that reaches the broker, with native writes counted as 1. One-batch coalescing for open-then-withdraw, open-then-Stop and answer-then-sent, asserting replay equals the runner and the native reply count.
    9. A phone-set YOLO, a paired-desktop answer's answeredBy, and a save that throws at accept time (findings 9 and 10).
    10. Phone tests for a pruned cursor, an ahead cursor, more than 10,000 events, and a reply without a journal. Ahead-of-log and foreign-id cursors through the desktop host.

    Open questions

    • With synchronous=NORMAL, a power loss can roll back rows that were already sent to clients. If later events then pass a phone's old cursor, that cursor would be accepted and skip events. This isn't proven, because I couldn't simulate a power loss.
    • Does codex-cli 0.163.0 echo answers in a tool item's item/completed result? If it does, codex.ts:466 would store JSON.stringify(item.result) unredacted. The repo has fixtures only, no live capture.
    • approveAlways on the phone sets Always allow before sending the answer (CompanionModel.swift:433-434). A 409 therefore leaves the thread on Always allow. Is that intended?

    Probes

    The probes are on Val's Mac, in the steward session's scratchpad: /private/tmp/claude-501/-Users-buns-Documents-GitHub-TypeSafeAI-modex/de146a50-44aa-48d6-b0d6-cddfd613e608/scratchpad/cf6-review/.

    • Contents: probes-a/ (coalesce, race, restart-resume, sent-before-write, yolo-attribution), probes-b/ (p1–p3) and probes-c/ (c1–c6).
    • How to run: each one imports the built copy of 21bb6cb from tree/. Run it with node <file>.
    • Lifetime: the folder is temporary, so copy anything you want to keep.

    Findings 1 and 5 are in code this branch didn't change, so they exist on main today. The others are in the request half itself. This review doesn't merge, push or change ownership of anything.

  2. self-assigned this
    on Oct 18, 2026
  3. BunsDev commented on Oct 18, 2026

    @BunsDev
    ContributorAuthor

    CF-6 claim: journal mechanics on main (a separate slice from cf6-requests), 2026-10-18

    Val asked this session (the docs steward, #290) to resume CF-6. cf6-requests is still claimed by session:cf6-requests, the #273 coordinator's stack, and stacked on unlanded CF-3 (#293), so I'm not touching it.

    This slice

    It goes in its own worktree, cf6-journal-hardening, cut from main at a4039b2. It covers only code cf6-requests doesn't change: event-journal.ts, desktop-host.ts, desktop-client.ts, plus tests and docs.

    • Comparison test. On one mixed transcript, journal-off reload, direct live projection, snapshot plus replay, paired-desktop catch-up and the phone's delta must agree.

    • Injected faults:

      • duplicate frames;
      • an old-attempt update;
      • a cursor from a deleted journal;
      • pruning;
      • future and expired cursors;
      • an oversized replay;
      • a journal write failure;
      • a crash immediately after an approval write.

      Each comes with counts of backend calls, to prove replay runs nothing.

    • Repairs, red first, for the review's findings that sit in main's code:

      • 1: a failed write during resume() drops a batch;
      • 2: a crash inside the batch window keeps the journal id;
      • 5: a frame over 1 MB loops forever.
    • Docs: docs/event-journal.md gets failure, crash-window and rollback/downgrade compatibility (finding 8).

    Left with cf6-requests

    The request-half findings, 3, 4, 6, 7, 9, 10 and 11, and everything in runner.ts, human-requests.ts and the iPhone request UI.

    Ground rules

    • MODEX_EVENT_JOURNAL stays opt-in.
    • The journal is not made authoritative.
    • I won't merge or release.

    The PR will link here, with exact SHAs, results and skips. If the coordinator already has these in hand, say so here and I'll stop.

  4. BunsDev commented on Oct 18, 2026

    @BunsDev
    ContributorAuthor

    CF-6 receipt: journal mechanics slice, PR #309 (2026-10-18)

    From the docs steward session, following its claim. The PR is open and not merged; merging needs Val's go-ahead.

    Ownership and scope

    • This slice: branch and worktree cf6-journal-hardening, cut from main at a4039b2. Main was merged in at 57e7607 (CF-3, Answer each approval and question once, and show Codex questions as cards (CF-3) #293). Head e973246, 11 signed commits.
    • Left alone: cf6-requests, still claimed by session:cf6-requests, the request half. It was not touched.
    • Unchanged here:
      • MODEX_EVENT_JOURNAL stays opt-in.
      • The journal is not authoritative.
      • Batching, retention and the 10,000-event, 4 MB and 8 MB limits are unchanged.
      • Nothing in runner.ts, human-requests.ts or the iPhone app changed.

    Schema (exact)

    • events: seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT, thread_id TEXT, type TEXT, event TEXT (JSON), with index events_thread (thread_id, seq). Unchanged since v0.1.0.
    • meta (key TEXT PRIMARY KEY, value TEXT):
      • id: the journal's identity;
      • pruned_through: the highest cursor retention deleted;
      • new: open, "1" from open until a clean close, then "0".
    • Identity rule: a journal opened while still "1" (a crash, or a failed write) takes a new id.
    • Item mapping: the phone's mobileItem maps a kind this build doesn't know to { id, kind: "notice", level: "info", text, at }.

    Defects fixed (review findings 1, 2, 5, 8), each red first

    # Was Now Red evidence
    1 A write failing in resume() dropped the batch silently; onError never ran; later resumes skipped it Any failed write reports through onError once and marks the journal failed (no more records, no resume); resume() answers reload resume threw SQLITE_FULL; with main's journal, the phone read got 500
    2 A crash within 250 ms kept the id, so a pre-crash cursor resumed past an answer on disk The open flag; reopened-open means a new id Same id (unit); same id in the real SIGKILL test with main's journal
    5 A frame over 1 MB caused an endless resumed-reconnect loop The client forgets its cursor before dropping the frame, so it reloads The second connection resumed
    8 An unknown kind went to the phone as null, and the phone rejected the snapshot An inert notice, without the item's own fields mobileItem returned undefined

    Tests added

    test/event-journal.test.ts:

    • a read-path write failure;
    • a crash reopen;
    • a deleted journal, with every other file under MODEX_HOME (drafts included) byte-identical through crash, deletion and failure;
    • an MCP route token in no event, journal row or file, through a real failing ClaudeBackend turn.

    test/journal-resume.test.ts (real host, phone server and desktop client over TLS):

    • an oversized frame;
    • a SIGKILL right after an approval write;
    • a write failure during a phone read, with index.ts's wiring;
    • cursors ahead of the log, pruned, and from a deleted journal, with a control case that resumes;
    • the 4 MB replay boundary;
    • a batch delivered twice;
    • the mixed-transcript test described next.

    The mixed-transcript test: one run, recorded by a live journal and by one that batches the whole run.

    • Hostile input:
      • a duplicated answer;
      • a late tool.update after tool.done;
      • an old attempt's late delta after a retry that reuses the native id;
      • a native withdrawal coalesced with its opening.
    • Views:
      • the saved files (journal off);
      • the runner;
      • the live stream;
      • both journals, each also replayed twice;
      • the phone's snapshot plus its delta;
      • a desktop that slept;
      • a restarted runner.
    • Result: every view agrees. The backend ran 2 times (the turn and its retry). The CLI heard yes once, and the withdrawn request settled no.

    test/conversation-migration.test.ts:

    • the unknown-kind fallback;
    • a real v0.1.0-schema journal: rows kept, id gained, replay unchanged, draft and state files byte-identical;
    • rollback: a file this build wrote, driven with v0.1.0's and v0.2.0's own SQL.

    e2e/event-journal.spec.ts: the app keeps its id across a clean relaunch, and takes a new one after its Electron process is SIGKILLed.

    Commands and results

    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 simulator (iPhone 16 Pro, dedicated and since deleted). The Mac was loaded: load average 25–68.

    Command SHA Result
    pnpm build && pnpm typecheck && pnpm test c4565a9 Build and typecheck passed. Root 109/109, 16/16. Desktop 1,611 tests: 1,595 passed, 16 skipped, 0 failed, 0 cancelled. Then 21/21, 10/10, 16/16
    The same 48eaaf9 (before the merge) 1,597 tests: 1,579 passed, 16 skipped, 2 cancelled by 5–6 s timeouts in untouched files. They pass alone: claude-lifecycle 25/25, demo 3/3
    playwright test with event-journal, companion, desktop-host-launch, store-desktop, workspace-store, human-requests e973246 15/15
    xcodebuild … -only-testing:ModexCompanionTests test e973246 45/45, 0 skipped
    apps/ios/ModexCompanion/Scripts/test-e2e.sh (MODEX_UI_TIMEOUT_SCALE=2) e973246 43/43 (one expected failure inside a passing test, a simulated screenshot failure)

    Skipped or not run:

    • the 16 opt-in or platform-specific unit tests;
    • 69 of 75 e2e specs, which run in CI;
    • live Claude or Codex;
    • a physical iPhone;
    • Windows and Linux;
    • the 8 MB slow-stream drop;
    • power loss under synchronous = NORMAL.

    Acceptance criteria (#279): what this slice settles

    Criterion State after #309
    Convergence across direct projection, snapshot and replay, and client catch-up Met for items and native withdrawal (#285, plus the mixed-transcript test). The request half's delivery states wait for cf6-requests
    Duplicate frames harmless; repeated deltas kept; old-attempt updates don't mutate new attempts Met
    Withdrawal and resolution survive batching and replay; reconnect never revives a dead card or sends a second decision Partly met. Coalesced withdrawal and crash-after-answer are covered. Open: cleanup of a card left open at a crash happens only when its thread loads (finding 3, runner.ts, cf6-requests)
    Cursor reset, pruning, replacement, out-of-range cursors, size limits and journal-off all reload correctly Met: a pruned, foreign or deleted, or ahead cursor; the 4 MB replay; a 1 MB frame; a write failure reached by reading; journal off. Untested: the 8 MB slow-stream drop
    A crash between save, broadcast and native send gives truthful state with zero re-execution; old snapshots migrate Crash after an answer: met (SIGKILL, zero runs, one decision). Migration from v0.1.0: met. Open: delivery: "sent" is recorded before the write (finding 4, cf6-requests)
    Capability mismatches keep a readable fallback; a legacy client can't approve an unsupported question The phone's unknown-kind fallback: met. The legacy approval refusal: on main (CF-3). Open: older paired desktops render nothing for question (finding 6)
    Privacy and retention kept; no credentials or sensitive bodies in the journal MCP route tokens: met. Retention: unchanged and documented. Open: the docs should say request text is stored verbatim (finding 12)

    Docs

    docs/event-journal.md covers:

    • the read-path failure;
    • the identity rule;
    • the 1 MB frame;
    • the phone's unknown kinds;
    • the new tests;
    • a new Compatibility and rollback section.

    That section says the schema is additive. It notes that v0.2.0 ignores open, and that v0.1.0 doesn't prune or issue cursors. It also says v0.1.0 and v0.2.0 still send plan and question to the phone as null, which the fix only prevents from this build on. Deleting events.sqlite is always a safe rollback.

    docs/status.md gains a "Journal faults (CF-6 #279)" line.

    Remaining native and human gates

    • live-provider acceptance with the journal on;
    • a physical iPhone;
    • Windows and Linux;
    • the slow-stream and power-loss paths;
    • human review.

    Next steps

    1. Harden the event journal against failed writes, crashes and oversized frames (CF-6, journal mechanics) #309: CI, then Val decides whether to merge.
    2. cf6-requests (owner) rebases onto main. It may conflict lightly in docs/event-journal.md and in companion.ts's mobileItem, where the default now follows CF-3's question case. It then takes findings 3, 4, 6, 7, 9, 10 and 11.
    3. Leave this issue open until those land.
  5. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    Checkpoint: CF-6 request half landed

    Landed: #308 squash-merged as d625eda on main (head e408f54, base 81798d7).

    What it contains:

    • The card is the record of its request.
      • An accepted answer saves answeredBy (desktop or phone) and delivery: "sending" before the CLI hears anything, then delivery: "sent" after the hand-off.
      • These travel as whole values in the existing item/item_update events through the journal, snapshots, paired desktops and the phone. There is no new event type.
    • Restart: a card still sending becomes uncertain. It is inert and never re-sent, and the card says Modex could not confirm the agent received the answer. Older answered cards are unchanged.
    • The window says "Answered on your phone." under a card the phone answered. The phone gets answeredBy, delivery and withdrawn on approvals.
    • Fixed: the phone could not act on a thread started on the Mac. Since Install verified macOS updates with durable draft recovery #181 (v0.0.12), these threads keep their draft's full UUID, and the companion's routes accepted only 8 hex characters. Answering, sending, setting the policy and archiving got a 404. CF-8's fidelity e2e found it.

    Red first:

    • the six test/request-journal.test.ts cases:
      • replay, once and twice;
      • a restart;
      • simultaneous desktop and phone answers (one accepted, the phone's a 409);
      • a paired desktop over TLS resuming after the decision;
      • an expired cursor;
      • a crash mid-send.
    • a secret kept out of every file with the journal on;
    • answerNote;
    • the UUID companion route.

    Checks:

    Still open for #279:

    • Answering questions from the phone, and structured-question capability negotiation.
    • Today's iPhone build reads only answer, so a withdrawn approval still reads as declined there. Fixing that needs a Companion app change.
  6. BunsDev commented on Oct 19, 2026

    @BunsDev
    ContributorAuthor

    Update: the request half landed (#308), and #309 is current with it (2026-10-19)

    Review findings that #308's description doesn't address:

    • 3: a card left open at a crash is closed only when its thread next loads;
    • 4: delivery: "sent" is recorded before the backend writes;
    • 6: no capability negotiation, so older paired desktops render nothing for a question;
    • 7: the iPhone shows a withdrawn approval as "Denied";
    • 9: attribution, for YOLO answers and paired-desktop answers;
    • 10: a failed save at accept time strands the request;
    • 11: the phone doesn't refresh after a 409.

    I haven't rerun the probes against main, so treat these as open until someone checks them. With #308 and #309 both in, they are what's left of this issue's acceptance criteria, together with the native gates: live providers, a physical iPhone, Windows and Linux, the 8 MB slow-stream drop and power loss.

  7. 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.

  8. 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

  9. 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

  10. 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

  11. 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.

  12. 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.

  13. 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.

  14. 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.

  15. mariscate commented on Oct 22, 2026

    @mariscate

    Refreshed goal prompts — 2026-10-22 (main at 0e674e7)

    On main: every CF-6 review finding (3, 4, 6, 7, 9, 10, 11). Finding 10 (#336) came after the v0.3.0 tag and is unreleased. The journal mechanics (#309) and the request half of replay are documented in docs/event-journal.md and docs/human-requests.md.

    Still open:

    None of the repairs has run against a live provider, Store 0.0.8 or a physical iPhone, so the acceptance boxes stay unticked.

    Long-running goal prompt

    Finish CF-6 (#279) acceptance in TypeSafeAI/modex. Read AGENTS.md, docs/event-journal.md, docs/human-requests.md (Across replay and reconnect) and this issue's findings table, then verify main's SHA. Create your own worktree. Code: the phone question path is shared with #276. Agree with that issue's owner which side lands the companion change, and don't duplicate it. Here, add the replay and reconnect tests for an answered or withdrawn question seen by the phone. Gates: write a runnable gate plan in docs/reviews, one section per gate, with steps, expected journal and cursor state, and pass/fail evidence. The gates are live Claude and Codex (Codex after its 2026-10-25 limit reset), a physical iPhone reconnect after sleep and a network change, Windows and Linux, the 8 MB slow-stream fallback (forced with a throttled client), and power loss (killing the process mid-batch, as a proxy, labelled as one). Run what this Mac can run, and record versions, SHAs and first-pass results. Gates that need Val or hardware wait for Val, with exact steps ready. Keep the opt-in flag, batching, retention and replay limits unchanged. Close only on evidence, or when Val defers a gate in writing.
    

    Follow-up / resume prompt

    Resume CF-6. Fetch main and read the latest comments here and on #276. Re-run the journal, replay and human-request unit tests and the companion e2e. Check which gates have evidence since the last checkpoint and which Val has deferred. Run the next gate that this machine can run. Post a checkpoint: SHA, gate table, open items with owners, and one next action.
    
  16. BunsDev commented on Oct 22, 2026

    @BunsDev
    ContributorAuthor

    Coordination with #276 (phone answers to questions). The #276 session is taking the companion change on worktree/branch cf3-phone-answers, cut from main at 0e674e7. That covers companion.ts and mobile-commands.ts routing phone answers through the runner's answerQuestion broker with the same refusals, a 409 when another client won, secrets kept out of logs and state, desktop/phone race tests, and the native iPhone question card. To avoid duplication, #279 should not land a second companion path. It can add its replay/reconnect tests for an answered or withdrawn question seen by the phone on top of that PR, which I'll link here once it's open.

  17. BunsDev commented on Oct 22, 2026

    @BunsDev
    ContributorAuthor

    The companion change for phone answers to questions is #373 (branch cf3-phone-answers, from main 0e674e7). It routes POST /v1/threads/:id/answer-question through the runner's answerQuestion broker with by: "phone": same refusals, 409 for the loser, secrets never logged or saved. It sends the native question card only to phones that name question in x-modex-item-kinds; older phones keep the notice. #373 already includes desktop/phone race tests in both orders, a phone-answered secret, and Stop. For #279, build on #373 rather than a second path: add the replay and reconnect tests for an answered or withdrawn question seen by the phone (journal delta, expired cursor, reconnect after the decision).

  18. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Checkpoint — 2026-10-23 (PR #388)

    Worktree and branch: cf6-acceptance, from main at ec7be57. Head is dbd2e81; the tests ran at 8bf909f.

    What #388 adds. It builds on #373's broker path and adds no second companion path:

    • four phone question replay and reconnect tests:
      • a phone that slept through the desktop's answer;
      • a phone that slept through Stop;
      • pruned, ahead-of-log and foreign cursors that must reload;
      • an older Companion, which gets only the notice.
    • an 8 MB slow-stream test;
    • a mid-reply kill e2e, as a power-loss proxy;
    • the journal suites on the Windows and Linux CI jobs;
    • the gate plan.

    Each phone test counts Codex's replies (always one) and checks the secret is in no file, journal row, poll or snapshot. The phone tests passed on their first run, because the behaviour already existed. Mutations to the built code show each test can fail.

    Gate First pass Owner
    G0 phone question replay/reconnect Passed —
    G1 live Claude (2.1.309, Haiku, journal on) Passed for typed items and Stop: both threads replay to their saved transcripts, also twice. No live approval went through yet. Next live run (Chat mode, approve on the phone)
    G2 live Codex Not run Next session, after the 2026-10-25 limit reset
    G3 physical iPhone, sleep and network change Not run Val
    G4 Windows / Linux Suites: Linux 50/50; Windows 49/50 (a test's signal assumption, fixed, rerun on #388). Apps not run CI, then Val
    G5 8 MB slow stream Passed —
    G6 power loss Passed as a SIGKILL proxy; real power cut not run Val, if wanted

    Findings, corrected in event-journal.md:

    • a stream dropped at 8 MB always reloads, never resumes, because what it missed is past the 4 MB replay limit;
    • the 15 s heartbeat also drops a stalled stream;
    • at a crash the journal can be ahead of the thread files.

    Checks (local, 8bf909f plus docs):

    • pnpm build: passed.
    • pnpm test: desktop 1,816 passed, 16 skipped, 0 failed.
    • pnpm test:e2e: 389 passed, 6 skipped; site 7 passed.

    Open: G1's live approval, G2, G3, the G4 apps and a real power cut for G6. Acceptance boxes stay unticked until they have evidence, or until Val defers them in writing.

    Next action: after 2026-10-25 9:35 AM, run G2 step 1 (MODEX_EVENT_JOURNAL=1 MODEX_LIVE_SMOKE=1 MODEX_LIVE_BACKENDS=codex npx playwright test e2e/live-smoke.spec.ts) from cf6-acceptance.

  19. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    CF-6 checkpoint: questions across replay and reconnect (2026-10-22)

    PR: #389, branch and worktree cf6-replay.

    #385 landed as 2eba641. A Codex Plan turn now runs in Codex's plan mode, which sends its plan as a plan message (nativePhase: "plan").

    Acceptance criteria after #389

    Criterion State
    Direct projection, snapshot plus replay and client catch-up converge Met, now including questions and Codex's plan. One typed turn: commentary; a question with a secret, answered on the phone while the paired desktop slept; and a plan through a failed attempt and its retry. It agrees on every path: live projection, saved files, event stream, live and fully coalesced journals (each replayed twice), paired desktop, both kinds of phone, and a restarted runner. Fixed, red first: the phone didn't receive nativePhase
    Duplicates harmless; repeated deltas kept; old attempts don't change new ones Met (#285, #309), and rechecked for the plan's reused native id plus a late delta from attempt 1
    Withdrawal and resolution survive batching and replay; reconnect never revives a card or sends a second decision Met for questions. Answered, withdrawn by Codex, or stopped while the phone was away: the delta closes the card, queued taps get a 409, and Codex hears one reply. A paired desktop resumes to the card answered on the phone, and its stale answer is refused
    Cursor reset, pruning, replacement, out-of-range cursors and journal-off reload Met for questions on the phone: a foreign, ahead or pruned cursor, and no journal
    Crash truth; old snapshots migrate Met for questions. Crash inside the batch window after a phone answer: the old cursor reloads and nothing is re-sent. sending at a crash reads as uncertain on both kinds of phone. v0.3.0 questions and plan load closed, with the draft byte-identical
    Capability mismatches stay readable; a legacy client can't answer a question Met. Fixed, red first: an older phone was told a question answered on the phone was "Answered on your Mac". An older paired desktop's approve or deny on a question is invalid (now a committed test)
    Privacy and retention No secret in any delta, journal row, event, paired-desktop stream, phone view or saved file. Limits unchanged

    Native iPhone tests:

    • QuestionCardTests adds 4 cases: a delta closes one card and keeps another card's draft; a withdrawn card sends nothing; a reload replaces the transcript; an uncertain answer stays closed.
    • The paired fixture now keeps an event journal. A new UI flow closes two cards on the Mac (answered, stopped) while each is open on the phone with a draft.
    • These passed on first run. They pin existing behaviour; they are not red-then-green repairs.

    Commands and results

    Environment: macOS, Node v24.22.0, pnpm 10.34.5, the iOS 27.0 simulator (dedicated iPhone 18 Pro).

    • b77fecd, before the rebase:
      • pnpm build && pnpm typecheck && pnpm test passed. Desktop unit: 1,821 passed, 16 skipped, 0 failed; the other suites all passed.
      • pnpm test:e2e: 388 passed, 6 skipped, site 7/7.
    • 8f55ad4: iOS test-e2e.sh (MODEX_UI_TIMEOUT_SCALE=2): 43/43.
    • 8c36029, after the rebase onto 2eba641:
      • build and typecheck passed;
      • 10 affected unit files: 111/111;
      • e2e codex-native-plan, human-requests, event-journal, companion, store-desktop: 16/16;
      • ModexCompanionTests: 60/60.
    • No flakes or hangs in these runs. Earlier in the session, on another branch (Run Plan mode on Codex in Codex's native plan collaboration mode (CF-3) #385), backend-health.test.js hung idle for about two hours under load and passed 5/5 alone. Questions across replay and reconnect on the phone and paired desktops (CF-6) #389 doesn't touch it.

    Still open (native and human gates)

    • live Claude and Codex with the journal on;
    • a physical iPhone reconnecting after sleep and a network change;
    • Windows and Linux;
    • the 8 MB slow-stream fallback;
    • real power loss.

    The TestFlight upload of a Companion build with the card is Val's. The acceptance boxes stay unticked until those gates run or Val defers them in writing.

    Next step: Val reviews #389 and decides on merging. After that, a gate plan in docs/reviews for the gates above. The steps for a native question are already in #281.

  20. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Update (64c4515, #388). G1 is now passed in full. A live Chat-mode Claude Code 2.1.309 (Haiku) turn asked to run one command, and it was approved in the window. After a clean quit, the journal's 13 rows replay exactly to the saved transcript. The card reads answer: yes, answeredBy: desktop, delivery: sent, and the command ran once.

    Still open, with owners:

    • G2, live Codex: next session, after the 2026-10-25 9:35 AM reset.
    • G3, physical iPhone: Val.
    • G4, the Windows and Linux app sessions: Val. Their journal suites already pass 50/50 on CI.
    • G6, a real power cut: Val, if wanted; otherwise defer it in writing.

    Next action: G2 step 1 after the reset.

  21. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    CF-6 checkpoint: final head of #389 (2026-10-22)

    #389 is at fa4b7cb on main 2eba641, with 10 signed commits. It waits for Val's go-ahead to merge.

    Added since the earlier checkpoint

    • The 8 MB slow-stream drop is now tested through the real host and TLS.
      • A paired desktop stops reading, and the host drops its stream once the backlog passes 8 MB. A probe logged 7.87 MB just before the drop.
      • Its reconnect from the old cursor would replay more than 4 MB, so it reloads (resumed: false, nothing replayed) to the answered card.
      • Its stale answer is refused, and Codex heard one answer.
      • The test passed on its first run: it pins existing behaviour.
    • Live runs with the journal on, recorded in docs/reviews/2026-10-22-cf6-gates.md:
      • Codex (codex-cli 0.163.0, a Plan turn). Its question was answered from a phone over HTTPS, and the late tap got a 409. The live projection, saved files, event stream, journal (once and twice), the phone's snapshot plus delta, and a restarted runner were identical. The phone saw final/plan.
        • Evidence: live-codex-journal.json, sha256 55ab91f8…dda9a.
      • Claude Code (2.1.309, haiku, chat mode). The phone approved touch probe-file.txt, its late deny got a 409, and every view agreed.
        • Agent mode (acceptEdits) ran the same command without asking, so there was nothing to answer from the phone.
        • Evidence: live-claude-journal.json, sha256 c43a4f4e…2e74.
    • Steps for the remaining gates, with pass/fail checks: a physical iPhone (sleep, network change, Stop while offline), Windows and Linux.

    Full gate on the final head fa4b7cb

    • pnpm build && pnpm typecheck && pnpm test: passed.
      • Desktop unit: 1,844 tests, 1,828 passed, 16 skipped, 0 failed.
      • Other suites: 117, 16, 21, 10 and 16, all passed.
    • pnpm test:e2e: 389 passed, 6 skipped, 0 failed; site 7/7.
    • iOS ModexCompanionTests: 60/60.
    • iOS paired UI (test-e2e.sh, MODEX_UI_TIMEOUT_SCALE=2): 43/43.
    • No flaky, hung or retried tests.

    Gate table

    Gate State
    Live Codex, journal on Passed
    Live Claude Code, journal on Passed
    8 MB slow-stream fallback Passed (automated)
    Power loss Proxy only: process death inside the batch window, and SIGKILL. Real power loss is open
    Physical iPhone, after sleep and a network change Waiting for Val (steps ready; needs a Companion build after #373, and TestFlight is Val's)
    Windows and Linux Waiting for hardware (steps ready)

    Next: Val reviews #389 and decides on merging. Then the physical-iPhone gate, run after a Companion build reaches TestFlight.

  22. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Val's decisions (2026-10-23), recorded in the gate plan on #388:

    • G4: the Windows and Linux app sessions are deferred. The CI journal suites (50/50 on both) stand as the evidence.
    • G6: a real power cut is deferred. The SIGKILL mid-batch run stands as a proxy, labelled as one.
    • G3: Val will run the physical iPhone sleep and network-change gate, using the steps in the plan.
    • G2: live Codex stays open until after the 2026-10-25 reset. This goal is paused until then.

    #388 goes to the merge queue once CI is green. Before this issue can close: G2 (next session) and G3 (Val).

  23. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Coordination between #388 (cf6-acceptance) and #389 (cf6-replay). The two were opened 25 minutes apart from the same base and overlap. #388 has auto-merge on and owns the gate plan with Val's decisions, so #389 will rebase onto it once it lands. When it does, #389 will:

    #389 stays unmerged until Val's go-ahead.

  24. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Checkpoint, 2026-10-23 (main 2eba641)

    Two CF-6 PRs, reconciled at Val's choice. #389 (cf6-replay, fa4b7cb) is the base and is in the merge queue. #388 (cf6-acceptance, 9f8ae8f) is rebased onto it and keeps only what #389 lacks:

    • Windows and Linux CI journal suites, with the Windows signal fix;
    • an in-app live journal check, plus a Chat-mode live approval;
    • a mid-reply kill;
    • three event-journal.md corrections.

    #388's duplicate phone tests and 8 MB test were dropped. GitHub retargets #388 to main when #389 merges.

    Correction: my earlier checkpoints said live Codex had to wait for the 2026-10-25 reset. #389 ran it live on 2026-10-22, so it wasn't blocked. I didn't check.

    Gate State Owner
    Live Codex, journal on Passed (#389) —
    Live Claude, journal on Passed: engine probe (#389) and in the app (#388) —
    8 MB slow-stream Passed (#389), with the heartbeat path noted (#388) —
    Power loss Proxies passed; a real power cut deferred by Val —
    Windows and Linux CI suites passed; the app sessions deferred by Val —
    Physical iPhone, sleep and network change Not run Val

    Checks on fa4b7cb + #388:

    • pnpm build: passed.
    • pnpm test: 1,828 passed, 16 skipped, 0 failed, 1 cancelled. The cancelled test is the claude-lifecycle worktree-stop test, which hit its 6 s timeout under full-suite load; on its own it passed 5/5.
    • pnpm test:e2e: 390 passed, 7 skipped; site 7/7.

    Open: the physical iPhone gate (Val). This issue can close once that has evidence or Val defers it in writing.

    Next action: after #389 lands and #388's CI is green, merge #388 with Val's go-ahead. Val's earlier go-ahead covered #388 before the rebase.

  25. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Checkpoint, 2026-10-23: both CF-6 PRs are on main

    Windows and Linux CI on #388's final head 8551a9c ran the journal, replay and request suites, question-replay included. Both passed 56/56 (run 39905321101).

    Gate State
    Live Codex, journal on Passed
    Live Claude, journal on (engine and app) Passed
    8 MB slow-stream Passed
    Power loss Proxies passed; a real power cut deferred by Val
    Windows and Linux CI suites passed 56/56; app sessions deferred by Val
    Physical iPhone, sleep and network change Open: Val

    Still open: the physical iPhone gate, owned by Val, with steps in the gate review. It needs a Companion build cut after #373 on TestFlight.

    Next action: Val uploads that build, runs the gate, and posts the results here. This issue closes on that evidence, or on Val's written deferral.

  26. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Clean re-run on main e123ccc (both CF-6 PRs merged):

    • pnpm build: passed.
    • pnpm test: exit 0. Desktop 1,831 passed, 16 skipped, 0 failed, 0 cancelled. Every other suite passed.

    The claude-lifecycle timeout cancelled in the earlier run did not recur. The only open CF-6 gate is the physical iPhone (Val).

  27. BunsDev commented on Oct 23, 2026

    @BunsDev
    ContributorAuthor

    Val deferred the physical iPhone gate in writing (2026-10-23), through the session's question prompt: "Defer it, close #279". The evidence that stands for it:

    The gate review keeps its steps, in case it is run later.

    Every CF-6 gate now has evidence or a written deferral:

    Gate State
    Live Codex, journal on Passed
    Live Claude, journal on Passed
    8 MB slow-stream Passed
    Power loss Proxies passed; a real power cut deferred by Val
    Windows and Linux CI suites 56/56 on both; app sessions deferred by Val
    Physical iPhone Deferred by Val

    Code is on main in #389 (4f44304) and #388 (e123ccc). A clean pnpm test ran on e123ccc. Closing.

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