Skip to content

feat(native-chat): Codex structured chat — full migration (host runtime, journal, wire, desktop + mobile clients) - #13438

Closed
brennanb2025 wants to merge 131 commits into
mainfrom
brennanb2025/codex-structured-mobile-write
Closed

brennanb2025 wants to merge 131 commits into
mainfrom
brennanb2025/codex-structured-mobile-write

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is stack part C2 and contains only the mobile write spine on top of C1 / #13416. It adds a persisted, kill-safe outbox with one in-flight dispatch, same-client-message-id retries, editable queued messages, and delivery-unknown bubbles that remain visible as unconfirmed rather than failing or disappearing.

It also wires durable approval and question cards through the existing native-chat components with expected-revision compare-and-set responses, Codex free-text answers, turn cancellation, model and option changes, and mobile authorization for the new mutation RPCs. Attachments reuse the existing bounded chunked clipboard upload RPCs; large or high-resolution images are resized on device through a three-rung dimension/compression ladder before any raw full-size base64 read.

End-to-end trace

  1. use-mobile-structured-session-entry.ts creates or resumes the structured tab, hydrates the read reducer, outbox, option controller, and attachment controller.
  2. use-mobile-structured-session-writes.ts persists the optimistic submission before accepting the composer send, dispatches exactly one agentSession.send, and reconciles journal submissions by client message id.
  3. mobile-structured-mutation-envelope.ts reproduces the host canonical SHA-256 fingerprint and uses the client message id as the send operation id, so every retry reaches the same idempotency slot.
  4. use-mobile-structured-agent-session.ts continues C1 cursor-resumed streaming and pagination; the optional history fence supplies the write checkpoint when a subscription resumes silently at head.
  5. mobile-structured-session-timeline.ts combines durable journal items, prompt cards, and optimistic outbox rows while reusing MobileNativeChatMessage, MobileNativeChatComposer, MobileNativeChatPermission, and MobileNativeChatQuestion.
  6. use-mobile-structured-session-mutations.ts sends approval/question responses with expectedRevision, calls agentSession.cancel, and updates options with agentSession.setOption.
  7. use-mobile-structured-attachments.ts and mobile-image-source-picker.ts downsample before the existing start/append/commit chunked upload flow, preserving bounded transport frames.
  8. On reconnect or app restart, mobile-structured-outbox-store.ts restores queued rows and converts interrupted dispatches to unconfirmed; the user explicitly retries with the original client message id.

Adversarial review follow-through

I rechecked every posted finding on #13381 and #13394 against this stacked tip. The inherited lower stack now includes thread-scoped prompt and translation state, acquisition cancellation/rollback and shutdown cleanup, sink unbinding during replacement, bounded geometric stream checkpoints and graceful flush, free-text questions and request-specific approval detail, image/provider echo and array reasoning translation, and production runtime teardown; their focused regressions are present on the branch. C2 also caught and fixed the production mobile RPC allowlist gap for the new send/cancel/respond/setOption methods.

Validation

  • Root pnpm run typecheck: passed.
  • Root pnpm run lint: passed, including native/type-aware quality, reliability, max-lines, skill, and localization gates; only pre-existing WorktreeJumpPalette hook warnings were printed by the non-deny first lint stage.
  • Root focused host + mobile RPC authorization: 37/37 passed.
  • Root full Vitest: 49,007 passed, 97 skipped. The run reported 10 failures; the C2 mobile allowlist failure was fixed and its 4-test suite reran green. The remaining 9 are unrelated environment/timing baselines: three root-directory fixture failures, two inherited provider-env count assertions, SSH GC timeout, Agent Map popover timing, GitHub project import timeout, and an AI Vault performance threshold.
  • Mobile TypeScript: passed.
  • Mobile oxlint: passed.
  • Mobile full Vitest: 445/445 files passed, 3,423 tests passed, 3 skipped.
  • Mobile formatting: every C2 file passed. A whole-directory check additionally sees the three untracked planning briefs used for this stack, which are intentionally not part of the commit.

Stack / retarget plan

Base is brennanb2025/codex-structured-mobile-read so the review diff is C2-only. After #13416 lands, retarget this PR to the branch that received C1 (normally main) without rebasing away the review history.

Deliberate deferrals

Push notifications, desktop structured-chat polish, and general availability remain later rollout work. This PR is the requested mobile write spine and does not change the legacy/native-chat send path when structured mode is not selected.

Pre-RC adversarial fix round

Fixing commits:

  • bbc6aba927 resolves the inherited C1 review blockers.
  • 8778fdedad resolves the C2 adversarial blockers.
  • ec917184c3 clears the exact changed-line React Doctor gate after the C2 refactors.
  • 19df0ced4c updates the root integration and cross-version fixtures to exercise the mobile create-by-intent contract.

C2 blocker-to-regression mapping:

  • Reconnect/fence stalls: outbox dispatches are generation-fenced and requeued on disconnect or fence replacement; tests cover refused-head fence advance, an old in-flight response, and an unresolved same-fence reconnect.
  • Ineffective unknown retries/order: the durable unknown observation is marked on explicit retry, the same client message id is reused, and an unconfirmed head blocks later sends; the write-hook regression covers all three.
  • Unknown-dimension image upscaling: the quality ladder no longer calls resize without trusted dimensions; the picker regression asserts zero resize calls.
  • Deleted final preview URI: only superseded quality rungs are deleted; the picker regression asserts the returned third-rung URI survives.
  • Discarded partial uploads: each completed chunked upload is appended immediately; the attachment-hook regression keeps the first image when the second fails.
  • Cross-session composer/options leakage: the structured view remounts by session id, option records and pending operations are record-scoped, and stale completions are ignored; composer and option-hook regressions cover switching away and returning.
  • Incorrect cancellation lifecycle: the host journals and tombstones a primary-turn lifecycle row, mobile hides it from chat and exposes Stop only while running; translator, timeline, and end-to-end integration regressions cover start/completion.
  • Dead disallowed free text: the native question component receives the durable prompt's free-text allowance and omits the input when false; the component regression asserts no TextInput.
  • Uncaught RPC failures: mutation failures are converted to a handled refusal and false result; the write-hook regression asserts the surfaced error.
  • Silent outbox-cap loss: persistence rejects entry 65 instead of slicing queued messages; the store regression asserts no storage write and a visible rejection.

Inherited C1 P1 evidence:

  • Exact E2EE v2 authentication compatibility — bbc6aba927: authentication remains the transcript-bound exact shape and capabilities move to a post-auth encrypted control, covered on both mobile and desktop.
  • Mobile trusted attach rejection — bbc6aba927, with full-suite fixture alignment in 19df0ced4c: mobile agentSession.create accepts only create intent; host-owned attach fields are rejected.
  • Pagination without a momentum dependency — bbc6aba927: a settled page unlatches immediately and the second page is admitted without a momentum event.
  • Approval command precedence — bbc6aba927: request command wins over a sibling reason, with a command-plus-reason regression.
  • Static/effect cleanup — bbc6aba927 (read stream), 8778fdedad (attachments/options), and ec917184c3 (changed-line React Doctor): the final changed-code gate reports 0 standard, 0 type-aware, and 0 React Doctor findings.

Validation after the fix round:

  • Root typecheck: passed.
  • Root lint: passed; only the pre-existing non-deny WorktreeJumpPalette hook warnings were printed.
  • Root focused structured/E2EE/wire/integration suites: passed.
  • Root full Vitest: 49,014 passed, 97 skipped; six unrelated local baseline failures remain (three root-directory fixture statuses, two inherited provider-environment assertions caused by duplicated Git prompt-suppression entries, and one GitHub project import timeout). No structured-session, E2EE, cross-version, or integration test failed.
  • Mobile TypeScript, oxlint, and tracked-file formatting: passed.
  • Mobile full Vitest: 448 files passed; 3,436 tests passed and 3 skipped.
  • Changed-code static analysis: passed with zero findings in all three analyzers; refreshed GitHub static analysis job 93345261515 is green.

The PR remains based on brennanb2025/codex-structured-mobile-read so the review diff stays C2-only plus the explicitly requested inherited fixes. After C1 lands, retarget to the branch that received C1 (normally main) without rebasing away this review history.

Pre-RC fix round 2 (dcd26ef677)

Fresh-review blocker-to-regression mapping:

  • Durable unknown retry/order: an explicit retryUnknown control can rerun only a matching durable unknown submission, reuses the same client message id without appending another submission, and then releases the next outbox head. Regressions: redispatches an explicitly retried durable unknown without appending a second submission and retries a durable unknown by the same id before dispatching later messages.
  • Cross-session mutation isolation: operation keys now include session identity and late completions are fenced by client/session/fence before changing the visible error. Regression: isolates mutation ids and late errors across session changes.
  • Cancellation lifecycle: graceful close and fence replacement emit ended through the translator before disposal, tombstoning the durable running marker. Regressions: flushes the final coalesced text before a graceful close plus the create/send/stream/approval/cancel/reconnect integration history assertion.
  • V2 capability propagation: authenticated sockets expose the channel’s live post-auth capability set instead of an auth-time copy. Regression: projects post-auth v2 capabilities into later RPC dispatches.
  • Acquisition teardown: attempts register before any asynchronous launch resolution, so closeAll() fences a pending resolver before it can spawn or publish. Regression: fences an acquisition while launch resolution is still pending.
  • Journal replacement draining: the sink unbinds during acquisition and every replacement awaits captured old-journal writes before opening the new journal. Regression: drains writes captured by the old journal before acquiring its replacement.
  • Stream unsubscription: direct and relay teardown derive agentSession.unsubscribe from the subscribe request id/session even though the stream emits no ready metadata; queued-unsent relay streams remain side-effect free. Regressions: unsubscribes a structured stream by its request-scoped subscription id and unsubscribes an agent session without waiting for nonexistent ready metadata.

Round-2 validation:

  • Changed-code static analysis: green with 0 standard, 0 type-aware, and 0 React Doctor findings.
  • Root typecheck and full lint gate: passed.
  • Structured host/adapter/runtime/RPC focused suite: 93/93 passed.
  • Root full Vitest was run: 4,550 files and 48,992 tests passed. Its 11 local failures reduced on a four-worker rerun to only the three pre-existing macOS Bash 3 root-directory-guard fixture failures; all other 85 rerun tests, including the structured end-to-end integration, passed.
  • Mobile TypeScript, oxlint, and formatting: passed.
  • Mobile full Vitest: 448/448 files passed; 3,439 tests passed and 3 skipped.

The base remains brennanb2025/codex-structured-mobile-read for a C2-only review diff; after C1 lands, retarget to the branch that received C1 (normally main) without rewriting the stack history.

Gate follow-up cbd79a61e9 moves the cross-session active-context ref update into an effect after the first refreshed CI run caught render-time ref mutation. The exact base-pinned React Doctor command now completes with warnings only and no blocking errors. Refreshed static analysis, typecheck, cross-version wire compatibility, root-directory guard, and mobile verify are green on cbd79a61e9.

Pre-RC fix round 3 (73c7f95457)

Final blocker-to-regression mapping:

  • Unjoined final journal writes: runtime shutdown now closes every adapter child and then awaits every deferred host event-sink chain, including on a close error. The end-to-end runtime integration regression holds a real journal append, proves stop stays pending, releases it, reopens the disk journal, and verifies the final streamed text survived while the running-turn marker was tombstoned.
  • Post-snapshot acquisitions: the acquisition registry latches closed synchronously before closeAll can yield, checks that fence after every acquisition await, and closeAll drains until both published sessions and acquisition attempts are empty. The dedicated shutdown regression executes gated A -> closeAll -> B -> release A, asserts both attempts reject, and proves no app-server child was spawned or survived.

Round-3 validation:

  • Changed-code static analysis: passed with 0 standard, 0 type-aware, and 0 React Doctor findings; max-lines ratchet passed with no bypasses.
  • Root node typecheck: passed.
  • Root focused teardown/adapter/translator/sink/host/runtime/integration suites: 7 files and 94 tests passed.
  • Base-pinned React Doctor command: completed without blocking errors.
  • Mobile TypeScript and oxlint: passed.
  • Mobile full Vitest: 448/448 files passed; 3,439 tests passed and 3 skipped.
  • Mobile tracked-file formatting: all 1,237 tracked files passed. The unfiltered directory command only flags the two intentionally untracked planning briefs, which remain outside this PR.

The base remains brennanb2025/codex-structured-mobile-read for the C2-only review diff. After C1 lands, retarget to the branch that received C1 (normally main) without rewriting the stack history.

Refreshed GitHub static analysis is green on 73c7f95457.

Pre-RC fix round 4 (8623f12)

Root cause: paired capability negotiation completed correctly and the Chat session action did invoke agentSession.create, but mobile minted durable operation IDs as mobile-structured: (and prefix:base36-time:uuid for later writes). The host authority accepts only <13-digit-ms>-<32-lowercase-hex>, so durable admission refused create before adapter acquisition and tab publication; the error then appeared only inside the no-tabs empty state, leaving QA with existing tabs a silent no-op.

Fix and regression mapping:

  • Durable create/write admission: createMobileStructuredOperationId now emits and locally validates the exact host-authority shape, and create, queued sends/retries, approvals, questions, cancellation, and option writes all use it. The mutation-envelope regression locks the accepted shape and rejects invalid timestamp/entropy inputs.
  • Visible create refusal: dispatcher failures preserve their concrete RPC message, while host refusals and transport errors render in a persistent accessible banner outside the empty-state branch. The mobile entry render test taps the Chat session callback, asserts agentSession.create and its valid envelope, then proves both a host refusal and invalid-params dispatcher refusal remain visible.
  • Paired create-to-send seam: the mobile transport integration authenticates the real paired E2EE client, projects its advertised structured capability into the real RPC dispatcher, creates through the real durable record store/host, sends a mobile outbox message with the returned fence, and verifies adapter dispatch. This fails against the pre-fix source at durable create admission.

Round-4 validation:

  • Changed-code static analysis: passed with 0 standard, 0 type-aware, and 0 React Doctor findings; max-lines ratchet passed with no bypasses.
  • Root typecheck: passed. Focused structured host/runtime integration: 2 files and 38 tests passed.
  • Root full Vitest: 4,561 files and 49,023 tests passed, 97 skipped. The only three failures are the pre-existing macOS Bash 3 root-directory-guard fixtures (the CI runner uses Bash 5); all structured, paired, E2EE, RPC, and durable-host tests passed.
  • Mobile TypeScript, oxlint, and tracked-file formatting: passed.
  • Mobile full Vitest: 449/449 files passed; 3,443 tests passed and 3 skipped.

The base remains brennanb2025/codex-structured-mobile-read for the C2-only review diff. After C1 lands, retarget to the branch that received C1 (normally main) without rewriting stack history.

Round-4 teardown addendum (5435cf7)

  • Post-acquire/pre-bind teardown: the host now tracks every in-flight attach separately from ordinary session mutations. Final runtime flushing repeatedly joins those attaches through journal open and sink bind before draining deferred operations, so adapter close rows cannot land after teardown reports complete.
  • Integration regression: the real create path is held in Codex historyFilePath after acquisition, provider running/final events are buffered, runtime stop begins, and the test proves stop remains pending until journal bind; the reopened disk journal contains the final text and no running lifecycle marker. Against the pre-fix source, stop resolves before the gate is released.
  • Focused structured host/runtime suite: 2 files and 39 tests passed. Root typecheck, changed-code static analysis, max-lines ratchet, and the paired mobile focused seam remain green.

Pre-RC parity round 5 (9202192f10)

Per-item fix and fail-before regression mapping:

  • Live model picker: added additive agentSession.options; the Codex adapter pages the app-server model/list catalog, filters hidden entries, and hydrates current model/default plus model-specific effort choices. Host tests cover paging/current values and mobile hook/picker tests prove the real catalog drives agentSession.setOption; the paired-mobile RPC allowlist regression covers the remote seam.
  • Effort: reasoning effort is a first-class live option from the same provider catalog, applied on the next turn/start alongside model. Adapter tests assert hydrated and changed effort reaches the next turn; mobile option tests assert selection dispatch and model-dependent effort reconciliation.
  • Structured slash dispatch: /model and /effort open or apply through the structured option flow; every other advertised Codex command renders an explicit “not available in chat sessions” response, while unknown unadvertised slash text remains a prompt. Command and render-seam regressions prove advertised commands never enter the outbox or call chat onSend.
  • Host-restart continuation: readable restore now adjudicates the durable lease; only a positively dead/released owner is replaced with a new generation, then the pinned Codex thread resumes before tab restoration. The cross-version integration regression restarts the host and verifies resumed write plus journal/cursor continuity, while indeterminate/live ownership remains read-only and never starts a competing writer.

A later parity audit also exposed that structured create bypassed normal selected-account launch preparation. The same commit now routes create through prepareCodexRuntimeHomeForLaunch, so the durable record pins the selected managed account and receives the existing project-trust, hook-install, and rollout-bridge preparation; orca-runtime-structured-agent-session-create-intent.test.ts fails on the old configured/system-home behavior and proves the selected prepared home wins. Desktop/status/orchestration projections remain outside this mobile C2/RC round, consistent with the instruction not to start part D.

Validation:

  • Root typecheck: passed.
  • Root full lint/static-analysis gate: passed, including native/type-aware audits, reliability, max-lines, skill, and localization gates; post-audit account wiring also passes targeted oxlint and formatting.
  • Root focused structured adapter/options/RPC/mobile-allowlist/restart/cross-version suite: 6 files, 64 tests passed. The earlier broader focused run passed 5 files / 90 tests and the cross-version suite passed 8 tests.
  • Root full Vitest was run: 4,556 files and 49,019 tests passed, 97 skipped. Its new allowlist failure was fixed and reran 4/4 green; the remaining nine local failures are unrelated Bash-3/environment/timing baselines (three root-directory fixtures, two inherited provider-environment assertions, relay grace, macOS helper owner-loss, transcript-watch liveness, and GitHub project import timing).
  • Mobile formatting, oxlint, and TypeScript: passed. Mobile full Vitest: 451/451 files passed; 3,451 tests passed and 3 skipped.
  • The device parity run posted GO for the personal-device RC. Question cards could not be live-triggered because Codex 0.146.1 exposes request_user_input in Plan mode while Default-mode support remains behind its upstream under-development feature; the durable translation/reply tests remain green and no account feature flag was changed silently.

The base remains brennanb2025/codex-structured-mobile-read for the C2-only review diff. After C1 lands, retarget to the branch that received C1 (normally main) without rewriting the stack history.

…types

Adds the shared contract for a durable agent session: an execution-location
scoped record (host + WSL distro + workspace id, folder workspaces and git
worktrees alike), a provider handle CHAIN keyed per provider, the pinned
account home, and the single-writer lease with its monotonic fence.

Adjudication is pure and fails closed: expiry alone never grants a second
owner, a bare pid match is not proof of life, and a conflicted claim stays
conflicted. The client-operation ledger's replay/conflict/expiry/capacity
rules move here so they can be persisted instead of held in memory.

Nothing is wired yet; these types are dormant until later parts adopt them.
Stores records, leases, and the client-operation ledger in one host-scoped
file written atomically (previous state kept as a backup, temp file fsynced
and renamed), so a kill during an rc-update restart leaves either the old or
the new committed state and never a torn lease. Reservation and operation row
commit in the same transaction, and mutations serialize through one queue so
two compare-and-swaps cannot observe the same pre-state.

Restart reconciliation marks every loaded lease unreconciled and grants no
writer until it is adjudicated: a proven-live owner is re-adopted with the
fence unchanged, a proven-dead one bumps the fence, and anything unverifiable
lands in recovery. Identity probing is PID-reuse-safe and fails closed —
Windows has no start time here, so a child that cannot echo its spawn token
qualifies for manual recovery only.

Dormant: no caller constructs the store yet.
95 tests over the durable record, the lease, and the probes. The invariants
worth naming: a lapsed deadline never grants a second owner (only observed
exit, an absent pid, or an identity mismatch does), a bare pid match is not
proof of life, the loser of a concurrent compare-and-swap is refused and
never spawns, records and the operation ledger survive a restart with the
fence monotonic across it, a conflicted claim stays conflicted, and native,
WSL, SSH, and folder-workspace sessions never share a scope.

Each test was mutation-checked: 22 plausibly-wrong implementations (stale
steal, re-adoption bumping the fence, trusting loaded leases, forks recorded
as resumes, dropped tombstones, guessed Windows start times, unrolled failed
transactions) were applied one at a time and every one turns the suite red.
…ceipts and epoch cursors (phase 1c)

Adds the host-side append-only journal that a native chat session will read and
write: one durable log per agent session, one reducer shared by live events and
replay, and epoch-qualified cursors so a reconnecting client either resumes
exactly or takes a clean snapshot reload.

Nothing is wired up yet. No RPC, no adapters, no UI: the journal is dormant
until a later phase feeds it, so behavior for existing sessions is unchanged.

- Append-only JSONL log plus an atomic snapshot that carries its own retained
  tail, so compaction is a single durable step and a crash mid-compaction leaves
  the log a recoverable superset.
- Sequences are assigned inside the same serialized append that makes the row
  durable, giving the (epoch, sequence) space exactly one writer with no gaps
  and no reuse. Every write carries the runtime fence; a stale fence is refused.
- A write-ahead submission row is durable before dispatch and doubles as the
  optimistic user bubble, so an accepted provider echo upserts into the bubble
  the user already sees instead of adding a second copy of their own message.
- Dispatch is a state machine: pending, accepted, rejected, or unknown. Unknown
  is a displayed state meaning "delivery unconfirmed"; Orca never re-sends on
  the user's behalf and never silently drops the message.
- Items upsert by stable provider identity with revisions and tombstones.
  Identity keying never uses text equality, so asking the same question twice
  stays two messages.
- Payload bounds keep a head plus the byte length and digest inline and push the
  remainder to a content-addressed blob store; crossing a bound is always
  marked, never a silent truncation.
- A row written by a newer schema puts the host into read-only mode rather than
  being skipped, so a newer client's rows are never dropped or compacted away.
- Legacy import hydrates a journal by running the existing transcript decoders
  unchanged and reading identity off the same raw lines they consume.
…store

A digest reaches the filesystem after a round trip through a row on disk, so a
corrupt or crafted row could name a path outside the store. Blob names are now
required to be a bare sha256, checked on both reads and writes.
…quence

Raw 0x00 bytes made git classify both files as binary, so their PR diffs
were unreviewable. The separator stays NUL; only its spelling changes.
Route every byte that can reach a PTY through the single-writer lease
adjudicator, so a session owned by one runtime cannot be typed into by
another. Nothing binds a PTY to a durable session record yet, so the gate
short-circuits and today's shells, legacy agent terminals, and unbound
sessions are untouched.

Gated paths: terminal.send RPC, the renderer IPC write paths
(pty:write / pty:writeAccepted), plugin sendTerminalText, the agent-prompt
paste, and ptyController.write as a backstop for runtime writes that never
passed a typed gate. Every yield inside a write - inter-chunk, the
text/suffix pause, the agent-prompt submit delay, deferred size
measurement - re-checks the fence, so a handoff mid-paste stops the
remaining chunks instead of interleaving two owners.

Refusals are typed and additive on the wire: an optional
agentSessionRefusal on RuntimeTerminalSend and on the existing
pty:writeUnavailable payload, refusal codes drawn only from the existing
agent-session error codes, and stream-input refusals mapped onto the
WriteUnavailable frame old clients already decode.
…e-enforce' into brennanb2025/native-chat-structured-base
Adds the request/refusal shapes every `agentSession.*` method speaks, the
four-field idempotency envelope and its lease admission, the item-key inverse an
upsert needs, and the `agent-session.structured.v1` capability. The protocol
version stays at 3: the surface is additive, and bumping it would strand every
paired device on the current release rather than degrade one feature.
One place where the lease, the journal, and the provider adapter meet: every
mutating call recomputes the fingerprint, admits through the durable operation
ledger, checks the lease, then acts, so no method can grow its own admission
rules. Journal recovery rehydrates an unreadable journal from provider history
into a fresh epoch and tells subscribers which reset they are seeing.

Attach adjudicates the leases it loaded from disk before granting any writer.
Without that step every lease stays unreconciled for the life of the process and
a restarted host refuses every persisted session forever.
Ten methods on the runtime manifest, including a paged `agentSession.history`
over the journal cursor and a subscribe that streams cursor-qualified batches.
A declared client that did not advertise `agent-session.structured.v1` is told
the whole surface does not exist, so an old client never sees a session it
cannot render; an in-process caller negotiates nothing and is served.
Extends the cross-version harness, which covered the terminal stream only, with
the structured surface. The old side's advertised capabilities and registered
method names are read from the extracted release checkout rather than written by
hand: a build cannot advertise a string its own source never names, so "the old
side does not have it" stays a fact about a real release.
…red-base' into brennanb2025/native-chat-1d-wire
The hand-written lease drifted the moment the record grew a field. Same reason
the fixture exists for the write-admission tests.
…' into brennanb2025/native-chat-structured-base
The existing app-server client is request-scoped: one deadline for the whole
session, and every inbound message that is not a pending response is dropped. A
structured chat session needs the opposite — a child that outlives any single
call, per-request deadlines, and both traffic directions, because Codex streams
a turn as notifications and asks for approvals by sending requests back.

Adds a connection that spawns the child, completes the initialize handshake,
and stays open until closed, keeping the same environment overlay and reap
semantics as the short-lived client. A refusal is now distinguishable from a
call that never settled, so callers can tell "Codex declined" from "the child
died" instead of reporting both as failure.
Codex blocks a turn until every approval and tool-input request it sent is
answered, but the answer arrives much later and through a durable journal item
id rather than the live JSON-RPC request id. This holds the live requests for a
session and turns a chosen option back into the reply Codex expects.

Three cases the journal boundary forces: one request can carry several
questions but takes a single reply, so answers are buffered until the request is
complete; a request this build does not model is refused rather than left
blocking or silently approved; and a prompt is forgotten before its reply is
written, so a second answer finds nothing instead of replying twice.
Implements the structured agent-session adapter for Codex: one long-lived
app-server child per session, started or resumed under the lease the host
already reserved, reporting the process identity the lease re-proves and the
provider handle link the journal binds to.

The thread the adapter reports is the one Codex actually opened, never the one
a client declared, and a resume that lands on a different thread is refused
rather than recorded as a resume. A turn is named from the start response when
Codex sends an id there and from the turn-started notification when it acks
first; when neither names it in time the submission settles unknown, which the
journal renders as delivery unconfirmed rather than failure. Only a refusal
Codex actually answered becomes a rejection.

Model, effort, and policy are per-turn overrides — there is no thread-settings
write — so an option set between turns is applied to the next turn/start, and
unlisted keys are refused instead of becoming arbitrary client-controlled
parameters.
Six findings from the review of the structured Codex session, all of them
about a session that is live in the map while the thing it names is not:

- A prompt is addressed by its own approval id, not by the tool item it is
  about. A shell bridge re-asks per command under one parent item, and the
  second ask used to evict the first, leaving that turn blocked on a request
  nobody could answer.
- A subagent runs its own thread over the same connection. Its `turn/started`
  no longer satisfies the root thread's waiter, and every event now carries the
  thread it actually came from.
- Publication is one step at the end of acquire: the process identity is in
  hand, the child is still alive, and the attempt still owns the session. A
  failure reaps only its own child, so a replacement already published keeps
  running.
- Codex talks before its thread is named. Those events wait in the acquisition
  window and are delivered in arrival order once the session is published,
  instead of being dropped.
- A broken stdin pipe ends the transport rather than one write: the child is
  reaped and the owner learns its lease is worthless.
- A death that reaches us through several listeners reports once, keeping the
  first cause rather than the generic exit that follows it.
A provider starts streaming the moment its process exists, and that moment
is inside `adapter.acquire` — before the journal is open and before the host
has registered the session. The adapter now receives a deferred sink whose
writes queue in arrival order and drain once the journal is bound.

One sink lives for the session rather than for one acquisition, so an adapter
can hold it across a re-acquire without noticing the new journal object. It is
bound last in `onAttached` so nothing the provider streamed can interleave with
the attach result the client is about to read, and a refused attach that never
reached a journal closes it instead of leaving writes nothing will drain.

The host was one line under the 300-line cap before this, so the mutation
admission pipeline moved to its own module. That is a straight extraction:
recompute fingerprint, admit through the ledger, check the lease, run the
plan — the same route, now somewhere a new method cannot sit next to.
Providers emit one notification per token. Journaling each one writes a row
and wakes every subscriber per token, so one long answer costs thousands of
appends and thousands of stream frames on a phone.

Deltas accumulate and flush on a 60ms window, and the row carries a SNAPSHOT
of the text so far rather than the increment — which is also what makes
dropping intermediate frames safe for a reconnecting client. All streams share
one timer, so the deadline bounds latency the same way at one wakeup per
window instead of one per stream. Lifecycle bypasses the window by flushing
first, so nothing can be journaled ahead of the text that preceded it.
Codex item ids are not stable across resume, so a journal keyed by them
cannot reconcile a recovered thread against a live one: the same message
would arrive under a new id and append a second copy of itself.

The durable key is instead the position the item holds — thread, turn, and
ordinal within that turn, counting message items only. Non-message items get
no ordinal at all rather than a number from a second counter, because a
counter a resumed history cannot reproduce is worse than no key: it looks
reconcilable and reconciles wrongly. Those rows fall back to a live-session
namespace that a recovered journal simply will not contain, which is correct,
since Codex does not persist them either.

Approvals and questions become journal rows the same way, naming the command
the item already announced so the user is not asked to approve an opaque id,
and one row per question rather than one row per request.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

PACKAGED FINAL GATE — run 31753097102 — NO-GO

Tested the arm64 adhoc artifact v1.4.183-adhoc.20260813231559 from run 31753097102, built from PR head 023f0dfb42dc712806deed36ac4a308e46ebcd80. The packaged Orca.app/Contents/MacOS/Orca was launched directly with CDP :9731, isolated user data, and a launchd-like environment containing only HOME, USER, LOGNAME, system PATH, SHELL, and TMPDIR; no shell-profile variables were injected. All UI interaction and screenshots used Electron CDP.

Packaged final gate

# Exact check Result Evidence
1 Fresh structured Codex session: empty pre-send timeline; launch-shell env inheritance; first send streams GO Before send, notification: row count was 0. Reply with exactly PACKAGED-GATE-PONG. completed as PACKAGED-GATE-PONG; no CODEX_LB_API_KEY error. After the turn, seven payload-bearing unhandled hook/rate-limit rows were visible, but the requested fresh pre-send timeline was empty.
2 Switch away and back: adopt live session, no new Codex spawn GO Structured app-server PID stayed 78619; PTYs stayed @@9cd4bb68 and @@2fe9497c; no new tab or process appeared.
3 Quit/relaunch with live terminals: re-adopt, no duplicate resume, no writer lock NO-GO Daemon PID stayed 33204, and the two shell PTYs reattached. The live structured TUI @@12ee396c returned as exited-to-zsh, while startup created @@5122aee0, a second native-chat-toggle tab running a fresh Codex. Resource Manager rose 3 → 4; the new tab visibly says --- previous session unavailable, started fresh ---. No writer-lock text was found.
4a Forward + reverse: reverse focuses Chat tab GO Open agent TUI created the transient terminal; Return to chat removed it and selected Codex Chat.
4b Reverse while a different workspace is active: no focus steal GO Reverse completed to owner native while active worktree remained other-workspace and its active terminal ID stayed unchanged.
5 Vault Resume in Worktree completes or shows explicit retry GO From Terminal 1, the expanded session action activated the existing structured Chat; no silent timeout or retry banner.
6 Zero-turn TUI guard GO Open agent TUI was disabled with tooltip Send a message first to open the agent TUI.; it enabled after the first completed turn.

Release blocker

The relaunch still reproduces the packaged duplicate-resume failure. Before restart there were exactly three daemon sessions: native shell @@9cd4bb68, other-workspace shell @@2fe9497c, and live structured TUI @@12ee396c (fg=codex). After normal app.restart() and a direct relaunch under the same environment/profile, there were four: the first two unchanged, old TUI @@12ee396c now fg=zsh, and new @@5122aee0 fg=codex. The tab strip contains two native-chat-toggle terminals plus Codex Chat.

Screenshots

Zero-turn guard and exact tooltip:

Zero-turn TUI guard

Launchd-like first send succeeds:

First structured send

Switch-back adopts the existing live session:

Switch-back adoption

Reverse selects the Chat tab:

Reverse focuses Chat

Cross-workspace reverse leaves the other workspace active:

No cross-workspace focus steal

Vault resume activates the existing structured Chat:

Vault resume

Restart-created fresh duplicate (previous session unavailable, started fresh):

Restart-created fresh duplicate

Original TUI tab after restart, with the prior session scrollback followed by a zsh prompt:

Old TUI exited to shell

Overall: NO-GO. Do not send this packaged build straight to Brennan; the live structured terminal still duplicates/fresh-spawns across restart.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

PACKAGED FINAL GATE v2 — run 31759132745 — NO-GO

Tested the packaged Orca-1.4.183-adhoc.20260814010000-arm64-mac.zip at SHA dcbf5a0194ffa22a3971739db62822dc7e825d70 under a stripped launchd-like environment with a remote-debugging port. CDP reported name=Orca, isDev=false, with no dev label.

# Check Result
0 Packaged identity / stripped environment / isDev=false ✅ GO
1 Live TUI + sleeping/dead second agent → restart: exact live re-adoption, no spawn, per-session dead-agent resume ❌ NO-GO — the original exact PTY/incarnation retained scrollback but Codex exited to zsh; Orca spawned a fresh PTY/incarnation. The second workspace regained a terminal, but the expected Codex-agent resume was not established.
2 Fresh structured session: empty timeline → send streams ✅ GO
3 Switch away/back → no spawn ❌ NO-GO — switching back created a duplicate PTY/tab and showed previous session unavailable, started fresh.
4 Toggle forward/reverse → reverse focuses Chat ✅ GO
5 Vault Resume in Worktree ✅ GO — activated the existing structured chat.
6 Zero-turn toggle disabled until first turn ✅ GO

Release blockers

  • Restart adoption: before relaunch, the live TUI used PTY suffix f0e7ab48, incarnation e4ef0342-4d7f-492d-a7e7-1084d3c8761f, owner PID 43402, child PID 43480, session 019ffdd3-8143-79a3-9b6b-17f8b087a239. After relaunch, its scrollback remained, but Codex had exited and Orca spawned PTY suffix 1949594f / incarnation e9cf5689-3c01-4e09-a49c-f2a8fad2584d, producing duplicate tabs.
  • Switch-away/back: returning to the original worktree spawned PTY suffix ec0b53f5; Resource Manager increased from 3 to 4. This independently fails the no-spawn requirement.

Screenshots

Zero-turn empty timeline

01-zero-turn-empty-timeline.png

Fresh session streams under stripped environment

02-launchd-send-stream.png

Switch-back duplicate spawn

04-switch-back-duplicate-spawn.png

Restart duplicate and original TUI exited to shell

06-restart-live-tui-duplicate.png

07-original-tui-exited-after-restart.png

Second agent established before the restart check

08-second-agent-established.png

Reverse focuses Chat and Vault Resume completes

09-reverse-focuses-chat.png

10-vault-resume-completes.png

Verdict: NO-GO. The packaged build passes the fresh-session, toggle/reverse, Vault Resume, and zero-turn checks, but fails both exact restart adoption and switch-away/back no-spawn behavior.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

VERDICT: PASS

Fix SHA: 20b8e3d84ba485eb9b4c887a5119bd3c672510a7

Instrumented diagnosis from the exact run 31759132745 artifact (stablyai/orca-adhoc:v1.4.183-adhoc.20260814010000, SHA-256 38e55c00fa0ad7b2fd0a8b468068be906fecbca8cc898d17ec442c37a864ed9e):

The first packaged recovery claim was rejected only because terminalPtyIncarnationsByPaneKey had not hydrated yet (persistedIncarnationId: null); the daemon-presented PTY ID/incarnation, workspace/tab/leaf surface, signed session claim, and PID-reuse-safe Codex-child PID/start-time proof all matched exactly.

The separate switch-away/back mismatch was also instrumented: both renderer PTY sources were absent (terminalLayoutsByTabId and ptyIdsByTabId), while main still reported the exact structured session owner. On restart that session can already be owner: "native"; on TUI ownership, agentSession.handoffStatus reports the exact owner PTY. The activation gate now suppresses only the matching structured session claim: native ownership is sufficient for that exact session, while TUI ownership still requires the session-exact returned PTY to exist in live inventory.

The restart fix accepts only a missing persisted incarnation during the proven hydration window. A non-null incarnation mismatch still rejects, and signed claim, exact workspace/tab/leaf/PTY, daemon incarnation, unique candidate, and Codex-child PID/start-time proof remain mandatory.

Captured packaged restart fixture:

  • Workspace: 4d68c30a-b7eb-4078-a1ca-e44e9fa75024::/Users/brennanbenson/orca/workspaces/orca/unified-gate
  • PTY suffix: fda34510
  • Incarnation: 4cf23679-8987-487d-a24c-dba3bed1b442
  • Tab: ced3bd39-262b-41f3-a446-92ceab4f938c
  • Leaf: d4e9d94d-8ec3-4d0d-8ca3-52730ba61c24
  • Root PID: 71039; Codex PID: 71464; start time: 1786671390000

Packaged validation used the exact artifact copy with stripped environment and Electron CDP. After repairing only the two duplicate, non-agent-owned tabs produced by pre-fix attempts, restart stayed at 3 terminal sessions with exactly one unified-gate terminal; switch away/back also stayed at 3 with exactly one unified-gate terminal.

Gates:

  • Focused adoption/process/surface matrix: 58/58 passed.
  • Cross-version agent-session wire tests: 10/10 passed.
  • Typecheck: passed.
  • Targeted oxlint and pre-commit oxlint/react-doctor/format: passed.
  • Reliability gates, max-lines ratchet, bundled-skill verification, skill manifest, localization catalog, and localization extraction: passed.
  • Full lint reaches localization coverage and fails only on unrelated existing src/renderer/src/components/native-chat/NativeChatMessageList.tsx:133:71 (jsx-text: "bytes").

@brennanb2025

Copy link
Copy Markdown
Contributor Author

PACKAGED FINAL GATE v3 — run 31764256105 — NO-GO

Tested the notarized arm64 artifact Orca-1.4.183-adhoc.20260814023805-arm64-mac.zip at PR SHA 20b8e3d84b by launching the packaged binary with isolated user data, CDP :9743, and a launchd-like environment containing only HOME, USER, LOGNAME, system PATH, SHELL, and TMPDIR. CDP reported name=Orca, isDev=false, with no dev label.

# Focused check Result
0 Packaged identity / stripped environment / isDev=false ✅ GO
1 Live TUI + done/sleeping second agent → quit/relaunch ❌ NO-GO — the exact PTY/incarnation/owner PID and scrollback were adopted with one claim and no duplicate spawn, but the Codex child exited to zsh; the sleeping agent also failed to resume.
2 Worktree switch-away/back ×2, no spawn either time ✅ GO — both returns retained PTY …245f192d, incarnation 88f85e19…, owner PID 1298, and one ownership claim.
3 Fresh structured session: empty timeline → send streams ✅ GO — pre-send notification: row count was 0; the response completed as PACKAGED-V3-PONG.

Release blockers

Live-TUI survival is still incomplete. Before restart, the structured TUI used PTY suffix 245f192d, incarnation 88f85e19-b8af-49be-ac16-17245f286120, owner PID 1298, Codex child PID 1462, and Codex session 019ffe2d-c8fa-7de3-a460-39cd698f8c6a. After packaged app.restart(), the main PID changed 17555 → 49344 while daemon PID 20064 stayed alive; the exact PTY, incarnation, owner PID, scrollback, and single claim survived and no new PTY/tab spawned, but child 1462 was gone and foreground remained zsh.

The done/sleeping agent did not resume. Its pre-restart record was state=done, origin=worktree-sleep, restoreOnTabOpenOnly=true, provider session 019ffe30-eef0-7180-8d28-d90fa5b415ba. Activating its workspace consumed the record and created only fresh shell PTY …ffe9cd59; agent status was empty and no Codex child existed.

Reverse focus, Vault resume, and zero-turn were skipped per the tight v3 scope because they passed on two consecutive artifacts and this delta does not touch them.

Screenshots

Fresh structured session, empty pre-send timeline:

01-structured-empty-timeline.png

Structured send streaming under the stripped environment:

02-structured-streaming.png

Second agent completed before being slept:

04-second-agent-ready.png

Switch-back cycle 1, no duplicate spawn:

05-switch-back-1-no-spawn.png

Switch-back cycle 2, no duplicate spawn:

06-switch-back-2-no-spawn.png

Live TUI immediately before restart:

07-live-tui-before-restart.png

Same adopted PTY/scrollback after restart, but Codex exited to the shell:

08-live-tui-exited-after-restart.png

Sleeping-agent activation produced only a fresh shell:

09-sleeping-agent-resumed.png

Overall: NO-GO. The claim-hydration delta stops duplicate spawning, but the required live Codex process and the separate sleeping agent do not survive/resume. Do not send this build directly to Brennan.

Full local report: /private/tmp/orca-packaged-final-gate-v3.qGvO6T/qa-report.md

@brennanb2025

Copy link
Copy Markdown
Contributor Author

PACKAGED FINAL GATE v4 — run 31767793897 — NO-GO

Tested the notarized arm64 artifact Orca-1.4.183-adhoc.20260814034730-arm64-mac.zip at SHA ca0050913f16 by launching the packaged binary with isolated user data, CDP :9754, and a launchd-like environment. CDP reported name=Orca, isDev=false, with no dev label.

# Focused check Result
0 Packaged identity / stripped environment / isDev=false ✅ GO
1 Live structured Codex TUI + separate sleeping agent → quit/relaunch ❌ NO-GO — the exact live TUI process survives, but recovery also spawns a fresh duplicate Codex tab, loses structured ownership, and the sleeping agent resumes only zsh.
2 Fresh structured session: zero notification rows through a full streamed turn ✅ GO — notification: count stayed 0; the response completed as PACKAGED-V4-PONG.
3 Worktree switch away/back → no spawn ✅ GO — the exact two-session inventory, TUI PTY, incarnation, and root PID were unchanged.
4 Toggle round trip sanity ❌ NO-GO — forward worked before restart; afterward the reverse action was replaced by a manual-recovery error and Retry proof failed.

Release blockers

The live TUI itself now survives exactly, but recovery still duplicates it. Before restart the structured TUI was PTY suffix 62df34d0, incarnation d8f371fc-23ac-48e7-a87d-8abf9c4b9ad3, daemon root PID 25633, and Codex PID 25666, running the real codex … resume 019ffe6e-c542-7be0-a016-925e19843be1 command. After main PID 10011 → 51468, daemon PID 10516, PTY/incarnation/root PID/Codex PID, foreground codex, and scrollback all stayed exact — but startup added PTY suffix 45ca22ff, root PID 52987, a fresh Codex process, and a second native-chat-toggle tab. Session count rose 2 → 3 before the sleeping workspace was activated.

The structured handoff then reported owner=none, phase=failed, stage=manual-recovery, with The resumed terminal lost its durable owner identity. Retry proof returned the same state, so the reverse toggle was unavailable.

The separate sleeping agent still does not resume. Its durable record contained provider session 019ffe70-cfc3-7032-a90e-5e7ec5898bcd, origin=worktree-sleep, state=done, restoreOnTabOpenOnly=true, and the Codex launch config. Activation consumed the record but created PTY suffix fef2da1f / PID 59583 as an idle zsh shell; after more than 60 seconds there was no child and no codex resume 019ffe70-cfc3-7032-a90e-5e7ec5898bcd command.

Screenshots

Zero notification rows before the turn:

01-structured-zero-notifications.png

Completed streamed turn, still zero notification rows:

03-structured-complete-zero-notifications.png

Switch-back before restart retains one TUI and the exact session inventory:

08-switch-back-no-spawn-before-restart.png

Original live TUI and scrollback survive restart, but a second native-chat-toggle tab is present:

12-original-live-tui-after-restart-with-duplicate-tab.png

Sleeping-agent activation yields only a shell:

10-sleeping-agent-resume.png

Reverse toggle is blocked by manual recovery:

11-toggle-roundtrip-blocked-manual-recovery.png

Overall: NO-GO. Do not send this packaged build to Brennan.

Full local report: /private/tmp/orca-packaged-final-gate-v4.6cqw4z/qa-report.md

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Packaged restart restoration verdict — GO

Identity

  • Commit: bfdf02ee394e948e6b9c12c5df4560f082553e7e (fix: reconcile packaged terminal restoration)
  • Branch: brennanb2025/codex-structured-mobile-write
  • PR: stablyai/orca#13438
  • Packaged base artifact: run 31767793897, Orca-1.4.183-adhoc.20260814034730-arm64-mac.zip
  • Exercised app: /private/tmp/orca-packaged-final-gate-v4.6cqw4z/unpacked/Orca.app
  • Patched app.asar SHA-256: b58b8b192141a1ae93a1f73bdb9e23d77ae62878051986c4671fa104e80e9f80
  • Runtime identity: name=Orca, isDev=false, CDP :9755
  • Main process during the final run: PID 52147; retained daemon: PID 10516

The actual packaged application was exercised through Electron CDP. After that run, only formatting and test-contract updates were made; no product behavior changed, and the production Electron bundle was rebuilt successfully.

Restored startup trace

  1. Durable hydration and daemon inventory

    • Main waited for first-window services and the managed-WSL barrier.
    • The structured-session host hydrated durable sessions and incarnation/ownership state before reconciling the daemon inventory.
    • The retained daemon reported the live structured TUI PTY ending in cd0b5ab3, while the separate sleeping record for provider session 019ffeb9-2661-71d2-b4d3-8bee81585bb9 remained durable and unconsumed.
    • The startup preparation is a memoized, one-time barrier that resets only after failure; uncertainty fails closed.
  2. Live PTY adoption and ownership rebinding

    • Renderer PTY reconnect completed before any sleep replay or default-shell creation.
    • The live PTY was rebound to its exact durable surface: tab 7916e5dd-7fa2-4264-bdc2-0197fe5a6be5, stable pane identity, and original PTY.
    • The daemon process and foreground codex process survived the main restart, and structured agentOwnership remained present.
    • Adoption rejects stale or ambiguous state: mismatched pane/tab identity, competing PTY bindings, missing live inventory, or an incomplete structured owner all block recovery rather than authorizing a second writer.
  3. Single structured-tab projection

    • Structured session snapshots were projected only after reconnect/adoption preparation completed.
    • The exact tab ID above remained present and no second TUI terminal tab was created.
    • The live TUI continued to render its pre-restart foreground session and scrollback.
  4. Sleeping-session replay and missing defaults

    • Only after the restoration barrier opened did activation inspect sleeping records.
    • The sleeping record created PTY ending in ed4eabd0 with an atomic pending-startup payload and a stable first leaf.
    • The startup payload was consumed only after the pane bound a concrete PTY.
    • Process inspection proved the real command ran: codex --dangerously-bypass-approvals-and-sandbox resume 019ffeb9-2661-71d2-b4d3-8bee81585bb9.
    • The durable sleeping record was then consumed. Default-shell creation is gated behind the reconciled state and runs only for a genuinely empty workspace.
  5. Structured lease proof after adoption

    • The post-restart reverse toggle succeeded.
    • Durable ownership moved from the adopted TUI to runtimeKind=native with claimStatus=live; there was no manual-recovery fallback.

Acceptance oracles

Required failure mode Packaged result
No duplicate tab PASS — exact tab 7916e5dd-7fa2-4264-bdc2-0197fe5a6be5 remained; no second TUI terminal tab appeared.
Ownership survives adoption PASS — reverse toggle completed and the durable lease became native/live.
Sleeping record runs a real resume PASS — PTY …ed4eabd0 ran the exact provider-session resume command; record cleared after bind.
Live foreground TUI survives restart PASS — PTY …cd0b5ab3, daemon PID 10516, foreground codex, and ownership survived.

Safe packaged evidence

  • Sleeping record resumed as Codex: sleeping record resumed
  • Live TUI survived restart: live TUI survived
  • Ownership rebound and reverse toggle succeeded: ownership rebound

Private baseline captures containing account or status details were intentionally not uploaded.

Verification

  • Focused restoration suite: 8 files, 630 tests passed.
  • Full repository suite: 50,688 passed, 107 skipped, 5 unrelated failures. The two inherited Git-config assertions and three load timeouts were rerun in isolation: 126/126 passed.
  • pnpm typecheck: passed.
  • pnpm build:electron-vite: passed.
  • Reliability manifest: 74 gates passed.
  • Max-lines ratchet: passed; no new bypasses.
  • Changed-line native/type-aware/React rules: 0 new findings on the scoped diff.
  • Commit hook: Oxlint, React rules, and formatting passed across all 25 committed files.
  • Exact committed diff formatting and git diff --check: passed.

Readiness audit

  • P0: none found.
  • P1: none found in this restoration change.
  • P2: none found in this restoration change.
  • SSH/remote/folder workspaces fail closed on inventory or identity uncertainty; folder-session membership remains explicit and no local filesystem/process assumption was added.
  • No mobile-facing wire, RPC schema, route, pairing, protocol-version, or persisted mobile state changed.
  • No Git command, path convention, shell launcher, native module, dependency, binary, updater, signing, or packaging contract changed.
  • Windows resume quoting remains covered for cmd.exe, PowerShell, Git Bash, and SSH workspaces.
  • Startup adds bounded one-time durable restoration and inventory work; it adds no polling, retry loop, listener, unbounded cache, or subprocess fanout.
  • Staleness guards cover readiness cancellation, pane/tab identity, exact PTY ownership, competing bindings, expected startup-payload identity, and in-flight gate cleanup.

Residual repository state outside this commit: two user-owned untracked artifact directories were preserved untouched; the PR currently reports merge conflicts against main, and new remote checks had not populated immediately after push.

Verdict

GO for the packaged-restart restoration fix at bfdf02ee39. The four prior packaged failure modes are resolved in the actual packaged application, with deterministic state-machine tests and privacy-safe visual proof. PR merge still requires resolving the pre-existing base conflicts and observing the newly triggered remote checks.

…ructured-mobile-write

# Conflicts:
#	mobile/src/transport/mobile-relay-rpc-session.ts
#	mobile/src/transport/mobile-relay-rpc-streams.ts
#	src/main/ipc/pty.ts
#	src/main/runtime/orca-runtime.ts
#	src/preload/api-types.ts
#	src/renderer/src/components/WorktreeJumpPalette.tsx
#	src/renderer/src/components/native-chat/native-chat-file-link.test.ts
#	src/renderer/src/components/native-chat/native-chat-view-types.ts
#	src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts
#	src/renderer/src/components/tab-bar/tab-bar-props.ts
#	src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
#	src/renderer/src/hooks/resolve-zoom-target.ts
#	src/renderer/src/i18n/locales/en.json
#	src/renderer/src/lib/browser-palette-page-entries.ts
#	src/renderer/src/lib/simulator-palette-search.ts
#	src/renderer/src/lib/workspace-tab-palette-search.ts
#	src/renderer/src/runtime/web-session-tabs-sync.ts
#	src/shared/tui-agent-startup.ts
#	src/shared/types.ts
@brennanb2025

brennanb2025 commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor Author

packaged final gate v5

NO-GO for artifact 7b8191557736 from Actions run 31776657104 (release v1.4.183-adhoc.20260814063414). The pristine packaged app reported isDev=false.

Restart matrix:

  • ✅ Structured Codex chat restored as exactly one tab; no duplicate structured tab was created.
  • ❌ Structured ownership did not survive the reverse handoff. Chat → TUI restored the conversation into an owned Codex PTY, but the TUI stopped at the hook-review gate; the rollout verifier timed out, killed the PTY, and returned: “Could not open the agent terminal — chat still owns this session.” Therefore the reverse toggle and round trip cannot complete.
  • ❌ The separate sleeping Codex agent did not resume after relaunch with a real codex … resume 019fff13-f54d-7bb3-a7bc-92363159a268 command. It was not projected into the restored main workspace, and opening Codex there launched a fresh plain codex --dangerously-bypass-approvals-and-sandbox command instead.
  • ✅ The separate live Codex TUI foreground survived relaunch and retained PACKAGED_V5_LIVE_READY.

Other matrix checks:

  • ✅ Fresh structured session streamed PACKAGED_V5_STRUCTURED_STREAM_OK; journal inspection remained at 0 notification rows through the hook-firing turn.
  • ✅ Worktree switch away and back created no additional target-worktree session.
  • ✅ Mid-session model changed to GPT-5.6-Terra and persisted after relaunch as gpt-5.6-terra / medium.
  • ❌ The required toggle round trip is blocked by the failed Chat → TUI ownership handoff above.

Structured stream and persisted model:

structured-stream-model-restored.png

Ownership handoff failure with verifier detail:

toggle-handoff-failed-details.png

Live foreground Codex TUI restored:

live-codex-foreground-restored.png

Sleeping-session miss: fresh plain codex launch instead of the expected resume command:

sleeping-agent-not-resumed.png

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Overnight exploratory QA — Codex structured chat candidate (artifact run 31776657104, adhoc v1.4.183-adhoc.20260814063414)

Automated overnight QA of the packaged candidate (pristine bundle, fresh profile, CDP). Everything tested EXCEPT the two known-failing seams (chat→TUI handoff, sleeping-session resume). worker_done summary for dispatch context in task transcript; this comment is the batched anomaly log.

ANOMALIES

A1 (P1) — Closing a structured chat tab is a silent no-op after app restart
Close (×) on any Codex Chat tab AND session.tabs.close RPC both return success (closed: true) but the tab never leaves the tab bar or the runtime snapshot. Reproduced on restored tabs AND a freshly-created post-restart tab, in two worktrees, 6+ attempts. Close worked BEFORE the first restart; broke after it and stayed broken across 3 restart cycles. No console error, no dialog, no daemon close event. Mechanism likely: agent-session close only relays ui:closeSessionTab to the renderer; the runtime snapshot keeps the tab and restoreStructuredTabsIfSupported (called on every session.tabs.listAll) re-publishes every session tab (shouldPreserveHeadlessMobileSessionTab returns true for all agent-session tabs).
close-tab-fails
Evidence: 26-close-tab-fails.png, 27-close-broken-4tabs.png.

A2 (P1) — Rapid sequential sends duplicate the last user message in the transcript
3 sends ~2s apart (Name a color, Name a second color, Name a third color): wire journal has correct pairing (Blue/Green/Red), rendered transcript shows Name a third color TWICE with Green under the first duplicate. Also seen with A/B/C sent while an essay was streaming (messages duplicated in DOM, only last answered). Persists across tab switches/re-renders.
rapid-send-duplicate

A3 (P2) — Split terminal becomes orphaned after restart
Split created second Terminal 1 (pty …6825b933). After restart: runtime tab list has 6 tabs, tab group order has 5; split terminal rendered nowhere, unreachable, and unclosable (A1). Survives daemon restart.

A4 (P2) — /model never opens picker; partial model names rejected
/model → always Could not open the model picker.; /model luna → luna is not an available model...; only exact slug /model gpt-5.6-luna works. /effort low|high works; /permissions, /ide etc. correctly refuse.

A5 (P2) — Attachments permanently disabled in structured sessions
Attach button dead even with text; no image paste path in structured transport (attachDisabled={disabled || Boolean(structuredTransport)}). Scope gap, flagging since images/attachments were in scope.

A6 (P3) — No thinking/streaming indicator during long first-token latency
What is 2+2? took ~6 min showing only Agent is responding + Stop — no reasoning chip, no progress. Model-side latency likely, but zero UI signal.

A7 (P3) — Codex TUI tab title polluted by typed input
After typing echo TUI_ALIVE/ into the codex TUI tab, tab title and sidebar row became echo TUI_ALIVE/help and persisted across restarts.

Not reachable in this config (not bugs)

  • Approvals: account approval_policy=never + danger-full-access — no approval surface to exercise.
  • Account switcher: only system-default codex auth; no managed accounts.

Passed (repeatedly)

Create/multi-turn/streaming/cancel-mid-stream/model+effort-changes/session-reopen/concurrent sessions (3+1 cross-worktree)/worktree switching/splits (pre-restart)/3× full restarts with full restore (tabs, PTYs, transcripts, post-restart send+stream+cancel all green).

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Screenshot evidence for the batched anomaly report above. Full set (27 pngs + REPORT.md) is in the worktree at mobile/qa-evidence-114eee22dd/overnight-codex-20260814/.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

A1 follow-up — root-cause evidence found: while re-checking after posting, session.tabs.listAll now returns agent-session tab ids DOUBLE-PREFIXED (agent-session:agent-session:desktop_mst...) for all 4 chat tabs in the overnight-codex-qa worktree (previously single-prefixed). The structured-tab restore/re-publish path is re-prefixing ids on each cycle, so the renderer's close (session.tabs.close with agent-session:<sessionId>) can no longer match the stored tab id — close returns closed:true (relay) but the tab is never removed from the runtime snapshot, and restore re-publishes it on every listAll. This also explains A3 (split terminal orphaned: same restore keeps runtime-owned leaves that the renderer no longer renders).

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing this older Codex chat stack: #16729 and #18074 already provide structured Codex chat and phone messages, approvals, and questions.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant