Skip to content

fix(codex): a Codex native chat that never sent a message reopens after restart - #22639

Merged
brennanb2025 merged 4 commits into
mainfrom
brennanb2025/r1-codex-unrun-thread
Sep 25, 2026
Merged

brennanb2025 merged 4 commits into
mainfrom
brennanb2025/r1-codex-unrun-thread

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​442 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​441
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​114 $\color{#cf222e}{\Huge{\mathbf{−}}}$​24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​90

Scope

Codex native chat only. Every behaviour change is in the Codex native chat launch path (src/main/codex/codex-structured-*). Out of scope, and unchanged:

The one shared file, src/shared/agent-session-provider-handle.ts, gains a way to mark an unused Codex thread as replaced. Only a chat's own never-used created handle may use it.

ELI5

If you opened a Codex native chat and never sent anything, the chat broke after an Orca restart. Orca tried to reopen the Codex conversation it had started, but Codex only saves a conversation after the first message. Codex answered "no rollout found", and every message after that was refused. Now, when Codex itself says it never saved that conversation, Orca starts a new one in its place and the chat works. A chat that did have a conversation still reopens that conversation, with its history.

What Changed

Before. At create time, Orca starts a Codex thread and records it as the chat's created provider handle (codex-structured-session-acquire.ts). Every later launch treated any recorded handle as resumable and sent thread/resume (codex-structured-launch-resolution.ts). Codex writes no rollout until the first input, so after a restart the resume of a chat that never ran a turn failed with no rollout found for thread id <id>. The failed attach released the lease, and the next send was refused with "The session has no live owner to accept writes."

After.

  • Codex's own answer decides. openCodexThread (codex-structured-thread-open.ts) starts a new thread only when all of these hold:

    • The launch says the head is this session's own creation.
    • thread/resume failed with code -32600.
    • The message ends with Codex's exact detail, no rollout found for thread id <that thread id>.

    Codex's app-server matches that exact text itself. Orca's own error-wrapper prefix is deliberately not part of the match, and a test drives Codex's raw JSON-RPC error frame through the real connection so the two cannot drift apart unnoticed. Any other resume failure is thrown as before, including a generic "not found", another thread id, another code, or a dead process. A chat with a real conversation is never silently restarted without its history.

  • Only a creation can be replaced. createCodexStructuredLaunchResolver sets supersedeIfUnsaved only when the chain head's origin is created. A resumed head (Codex already resumed it once, so a rollout existed), a forked head or an adopted head (an imported conversation) never gets the fallback. There, a failed resume stays a visible failure.

  • The handle chain models the replacement as a supersession. A link gains an optional supersedesKey, the key of the handle it replaced, alongside the existing forkedFromKey. The new thread's link is origin: 'created' with supersedesKey. It replaces the unsaved creation in place (appendAgentSessionProviderHandleLink → supersedeUnsavedCreation), so the chain holds one live identity and does not grow across restarts. Before this change, the chain refused a second created link outright (agent_session_provider_handle_invalid).

Handle-chain invariants (all kept; the last one is new)

  1. A chain starts with exactly one created or adopted link, and no later link is created or adopted.

  2. Every link has the same provider.

  3. mintedAtFence never decreases along the chain.

  4. A resumed link keeps the identity root. Landing on another root is a fork, and is refused as one.

  5. A forked link has a new root and names the head it was seeded from (forkedFromKey).

  6. Re-proving the same handle at the same fence is a retry and adds no link.

  7. Link ids are unique, because the lease names its proof by link id.

  8. At most 256 links.

  9. New: a created link with supersedesKey may replace the head only when all of these hold:

    • The head is itself created, which by (1) means it is the only link.
    • supersedesKey equals the head's key.
    • The new link has a different identity root, a new link id, and a fence at least the head's.

    The result is a one-link chain. supersedesKey is only valid on a created link. A persisted chain of [created, created+supersedes] is invalid, so the replacement cannot be stored as a second live identity.

The lease keeps its current meaning. proveOwner sets provenHandleLinkId to the new head, and isAgentSessionRecord's live-lease check (head link id equals the proven id, at the lease's fence) holds unchanged.

Why

  • Why Codex's answer rather than a file scan. The launch already scans CODEX_HOME/sessions to pass path, and that scan stays. But Codex is the authority on whether it can resume a thread. Its own lookup covers layouts Orca's scan does not. A scan that missed a real rollout, for example after a storage-layout change, would start a new thread over a real conversation. That is the silent history loss this fix must never cause. With Codex's answer, a missed scan only means Codex looks the thread up itself. A change to Codex's message text makes the fallback stop matching, so the failure stays visible.
  • Why not a journal-derived "a turn ran" fact. Orca could also work out from the chat journal whether a turn ever ran under the thread: while the chain head is still created, every turn went through Orca's own chat path, because a terminal handoff records a resumed link and the fallback stops applying after that. It would still be a second, stored copy of a fact Codex already owns, and the two can disagree. Codex is the only authority on whether it can resume its own thread. Checked beside Codex's answer, the journal fact would change the outcome in one case only: a saved conversation deleted outside Orca before the chat's first restart (see Known limit), where Codex has nothing left to resume either way.
  • When the fallback can fire at all. When Orca's rollout scan finds a file, the launch passes its path, and Codex resumes by path. Supersession therefore needs two independent misses: Orca's scan finds no rollout, and Codex's own lookup by id finds none either. Codex uses the same "no rollout found" text for an archived thread read active-only, but its resume reads archived threads too and answers "is archived" instead. So on the resume path the text means no rollout exists at all, and an archived thread stays a visible failure.
  • Why narrower than "fall back on any resume error". A blanket fallback drops the history of a real conversation whenever its resume fails for another reason. This fallback needs Codex's exact proof for the exact thread, on a thread no resume ever proved. An ablation below makes the match blanket and a test goes red.
  • Why supersede in place rather than append. The replaced thread never held a conversation, so nothing can continue it. Keeping it as a second link would present two identities. It would also add one link for every restart of a chat that stays unused, up to the 256-link cap. The existing Claude resume-point revision already rewrites the head in place for the same reason. supersedesKey names only the thread it directly replaced. Across repeated restarts of a never-used chat, only the most recent superseded thread id is kept; earlier discarded ids survive only in the operation ledger, until the ledger evicts them (24 hours, 512 operations per client, 4096 overall). None of them ever held a conversation.
  • Consistent with the Claude half (fix(claude): open structured chat without a startup deadline, and make Retry start fresh #22364). That PR launches Claude fresh with the same --session-id when Claude wrote no transcript, decided from Claude's own storage. Codex mints its own thread id and cannot reuse the old one, so it needs the chain supersession. The rule is the same in both: resumability comes from whether the provider actually saved the conversation, not from Orca's record of the handle.

The send refusal ("no live owner")

That refusal comes from the shared, provider-agnostic admission path: refuseUnlessWriterAdmitted in src/shared/agent-session-mutation-envelope.ts, reached from the host's mutation admission for both Claude and Codex. #22364 replaces that path for both providers:

  • A send to a cleanly released lease runs the shared restart (ensureProviderChild).
  • A failed restart returns agent_session_owner_restart_failed, which names the cause, for example "Codex couldn't restart: codex app-server thread/resume failed: …". It also writes the same status row into the chat.

Adding a second cause-carrying refusal here would put a parallel mechanism beside that one on the same shared path, and they would conflict. So this PR leaves the refusal to #22364. After this PR, the unrun-chat case no longer fails at all. A real Codex resume failure gets its cause shown through #22364's path. #22364 is now on main, so that path is live.

Linked Issue

None — part of the structured chat status/orchestration program.

Visual Proof

Hidden-window Electron validation (ORCA_BACKGROUND_LAUNCH=1, CDP screenshots), real Codex app-server, real ~/.codex.

Before (main 122b8c2): a Codex chat that never ran a turn, after restart, then a send.

shot-05-unrun-codex-send-refused.png

After (this branch): a new Codex chat, nothing sent, app restarted, then a send. Codex answers.

shot-06-unrun-send-after-restart.png

Control (this branch): a Codex chat that ran one turn before the restart resumes the same thread, and Codex recalls its earlier reply.

shot-04-control-resumed-remembers.png

After a second restart: the formerly unrun chat now has a saved conversation. It resumes normally and recalls its reply.

shot-08-formerly-unrun-chat-remembers.png

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Live run (macOS, dev build of this branch, isolated profile). Durable records read from agent-sessions.json after each step:

Step Unrun chat codex_5c9ec790 Control chat codex_ebbd5c65
Created [created 01a0d2f8-8ad7…]; no rollout file for that id. The positive control, other rollouts from today, is present. [created 01a0d2f8-f5a8…]; one turn sent, rollout written
Restart 1, tab shown live fence 3, [created 01a0d2fb-375d… supersedes 01a0d2f8-8ad7…]. A send is answered and a rollout is written for the new thread. live fence 3, [created, resumed] on the same thread; recalls its earlier reply
Restart 2, tab shown live fence 5, [created 01a0d2fb… (supersedes …), resumed 01a0d2fb…]; recalls its earlier reply —

A Codex chat created on main, codex_b97206fa, was also never sent to. It superseded its unsaved thread on each of both restarts, and its chain stayed one link long.

Automated.

  • New tests:
    • agent-session-provider-handle.test.ts (supersession rules)
    • codex-structured-thread-open.test.ts (exact-proof fallback, and no fallback for any other failure)
    • codex-structured-launch-resolution.test.ts (only a created head is supersedable)
    • codex-structured-session-adapter.test.ts (the link names what it replaced)
    • agent-session-unsaved-creation-supersession.test.ts. This runs the real record store: prove, restart, supersede, restart, read back. It also checks that the store refuses a supersession over a resumed conversation.
  • Related suites: src/main/codex, src/shared/agent-session*, src/main/runtime/agent-session*, src/main/runtime/structured-agent-session*, src/main/native-chat/agent-session-wire. That is 344 files and 3429 tests, all passing.
  • pnpm tc:node passes, and full pnpm exec oxlint exits 0.

Ablations. The fix was committed first, then each part was removed and the tests re-run:

Arm Change Result (of 82 tests in the 5 touched files)
0 fix as committed 82 pass
A all fix source reverted to main 6 fail. The store test fails with agent_session_provider_handle_invalid, which is the chain bug. The adapter and thread-open tests fail with no rollout found for thread id thread-unsaved.
B chain supersession rule deleted 2 fail (chain + store)
C Codex no-rollout fallback deleted 2 fail (thread-open + adapter)
D resolver never marks a created head 1 fails (launch resolution)
E adapter link does not name the replaced thread 1 fails (adapter)
F fallback made blanket (any resume error) 1 fails ("treats no other resume failure as proof")
G supersession allowed over any head origin 2 fail (chain + store refuse-over-resumed)

Review follow-up (6c91609bf5). The match no longer includes Orca's own wrapper prefix, and a new test sends Codex's raw error frame through the real connection. The link builder now rejects a supersession on an adopted or resumed link at the type level. Ablations of that commit (67 tests across the thread-open, adapter and connection test files):

Arm Change Result
W1 Orca's wrapper prefix reworded 67 pass: the match no longer depends on it
W2 wrapper reworded so Codex's text is no longer the suffix 1 fails: only the raw-frame test; every test that builds the error string itself stays green
M the previous exact-match rule plus a reworded prefix 1 fails: only the raw-frame test
T link-builder input type reverted pnpm tc:node fails on both @ts-expect-error pins

Arms A–G ran at 9b44721706. The follow-up commit e3903103cf only rebuilt the launch-resolution test's chains without a type assertion. Arm D was re-run at that head and still fails.

Review

  • SSH / remote / WSL. The decision is made by the Codex app-server on the host that runs it, against that host's own rollout storage. The launch resolver already refuses a structured Codex session that is not on its own host (executionHostId !== local or a WSL distro), so the resolver and the app-server always share one execution host. A paired remote Orca server runs this same code in its own runtime. Nothing crosses the wire. The handle chain lives only in the host's record store, and no RPC or stream frame changed.
  • Folder workspaces. Not workspace-kind specific. The live run used a git-worktree workspace, and the store test uses a folder workspace.
  • Mobile. No client change. Mobile sends reach the same host path.
  • Backward compatibility. A record written by this build reads back in an older build as a one-link created chain, because older validators ignore supersedesKey. No record written by an older build changes meaning.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Merge order

Notes

Not verified:

  • Codex on WSL or on an SSH-paired host was not run live. Structured Codex is local-only in the resolver.
  • Windows and Linux were not run live. The change has no platform branches.
  • The terminal-handoff path (codex resume <thread> in an agent terminal) still passes the recorded thread. It is not exercised for a chat that never ran a turn and is not changed here.
  • In the run where a supersession happens, journal submission rows are stamped with the superseded thread id. journalIdentityFor takes the chain head when the attach begins, and that attach lasts for the whole run. This predates this PR: a fork is stamped the same way. The stamp is informational, and no reader keys on it today. Follow-up: re-derive the journal identity from the proven link once the owner is proved, for supersessions and forks alike.

Future direction: starting the provider thread with the first turn, instead of eagerly at create, would remove this class of bug for both providers. That changes when models and options are published, so it is out of scope here.

Known limit: a created thread whose rollout the user deleted outside Orca, before the chat's first restart, is also answered "no rollout found". It starts a new thread. After the first restart the head is resumed, and the fallback no longer applies.

A structured Codex chat records its thread at create time, but Codex writes
no rollout until the first input. After a restart, launch resumed that
thread, Codex answered "no rollout found for thread id", and the chat could
never run again.

When the head of the handle chain is the session's own creation and Codex
answers that exact error for that exact thread, start a new thread instead.
The new link supersedes the unsaved creation in place and names it, so the
chain keeps one live identity and does not grow across restarts. A thread a
resume, fork or adoption proved is never superseded, and no other resume
error starts fresh.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The launch resolver enables supersession only for a resumed thread with a created provider handle origin. If the resume request returns the exact no-rollout error for that thread, the opener starts a replacement thread. The session acquisition path records the replaced thread in the new provider handle link. The provider handle chain validates supersession and replaces the unsaved created link. Added tests cover eligibility, error matching, handle validation, and persistence across store restarts.

Merge Risk: 🔵 Low · up to ef6fb

A narrowly shaped resume error could start a replacement thread when the error should instead be reported. Tighten the message check before merging, or accept this bounded risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the Codex native chat restart bug and the fix. It is concise and directly related to the main change.
Description check ✅ Passed The description is detailed and covers the user impact, implementation, rationale, scope, visual proof, testing, compatibility, and known limitations. The Linked Issue field says “None” although the t…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the full PR: how a Codex chat whose thread was never saved is restarted after an Orca restart, and how that replacement is recorded in the durable provider-handle chain.

  • Codex's own no-rollout answer gates the restart — openCodexThread starts a fresh thread/start only when supersedeIfUnsaved is set and thread/resume failed with method === 'thread/resume', code === -32600, and the exact message codex app-server thread/resume failed: no rollout found for thread id <id>; any other failure is rethrown unchanged.
  • Only a creation is supersedable — createCodexStructuredLaunchResolver sets supersedeIfUnsaved only when the chain head's origin is created; resumed, forked, and adopted heads keep their failure visible.
  • Supersession replaces in place — appendAgentSessionProviderHandleLink routes a created link carrying supersedesKey to supersedeUnsavedCreation, which validates that the head is created, the key names exactly the head, the root differs, the linkId is new, and the fence does not decrease, then returns a one-link chain.
  • The record store stays consistent — the acquisition reports resumed: false plus supersededThreadId, so the new link is a created link with supersedesKey; proveAgentSessionOwner repoints provenHandleLinkId at the new head, and the load-time validator accepts the one-link form while rejecting a persisted two-link [created, created+supersedes].

I traced the error plumbing (the record dispatcher composes the exact wrapper message the matcher expects, matching the existing -32600 / no rollout found for thread id fixture), confirmed resolvePinnedCodexRolloutProof returns null rather than throwing for a missing rollout, and checked every production consumer of providerHandleChain (the origin === 'adopted' first-link check, the mintedAtFence === runtimeFence owner probe, restart reconcile/rollback, and the ownership index). A specialist independently probed for a reachable non-idempotent re-prove, a dangling provenHandleLinkId, or an invariant violation and found none. The new tests pin exact behavior rather than loose assertions, including the ablations the PR describes.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…e real connection

The fallback matched Orca's own error-wrapper prefix too, and every test built
that string itself, so rewording the wrapper would have disabled the fallback
with the suite green. Match the method, code -32600 and Codex's exact detail
as the message suffix, and drive Codex's raw error frame through the real
connection in a test.

The link builder now refuses, at the type level, a supersession on an adopted
or resumed link, which the chain would reject downstream anyway.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This incremental run reviewed the delta since the prior pullfrog review, commit 6c91609bf5 ("match only Codex's own no-rollout text, pinned through the real connection").

  • Match Codex's no-rollout proof by suffix — isCodexNoRolloutError now tests error.message.endsWith('no rollout found for thread id <id>') instead of the fully wrapped string, so a reworded Orca error prefix can no longer hide the proof; method === 'thread/resume' and code === -32600 still gate it, and the trailing thread id (with its preceding space) keeps a longer, different id from matching.
  • Pin the proof to Codex's raw frame — a new test drives Codex's raw JSON-RPC error frame through the real openCodexAppServerConnection, so the match cannot silently drift from Codex's wording.
  • Restrict supersession to a creation at the type level — codexProviderHandleLink now takes a discriminated input union that rejects supersedesThreadId on an adopted or resumed link (checked by @ts-expect-error pins), with codex-structured-session-acquire passing resumed: false alongside supersededThreadId.

Verified in this run: the four touched test files pass (70 tests, including the raw-frame case) and pnpm tc:node exits 0, so the type pins are live.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Require exact error-message equality before replacement. · codex-structured-thread-open.ts:105-122

src/main/codex/codex-structured-thread-open.ts:105-122
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require exact error-message equality before replacement.

endsWith also matches messages such as context: no rollout found for thread id ${threadId}. The reachable thread/resume error path then calls thread/start and replaces the thread instead of propagating the unrelated error. Compare the complete message with the exact fallback text.

Suggested fix
-  return error.message.endsWith(`no rollout found for thread id ${threadId}`)
+  return error.message === `no rollout found for thread id ${threadId}`

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: af1dfc0e-241e-4e08-b0c9-9a7c2a8117c3

📥 Commits

Reviewing files that changed from the base of the PR and between 6c91609 and ef6fb34.

📒 Files selected for processing (1)
  • src/main/codex/codex-structured-thread-open.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/codex/codex-structured-thread-open.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@brennanb2025 brennanb2025 changed the title fix(codex): start a new thread when a chat's thread was never saved fix(codex): a Codex native chat that never sent a message reopens after restart Sep 25, 2026
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Electron QA (independent validation agent)

Independent live run of the scenarios in the QA spec, on this branch at ef6fb34cbf, hidden-window dev build with an isolated profile, real Codex login, structured chat flags on. Two structured Codex chats in one worktree: chat A created and never sent to, chat B sent one turn (remember the word BANANA, reply OK → OK). Two restarts with the same profile.

Unrun chat answers PINEAPPLE after the first restart, with no refusal or error row.

Control chat recalls BANANA after the first restart on the same thread.

Formerly-unrun chat recalls PINEAPPLE after the second restart.

Results: scenario 1 PASS (chat A answered PINEAPPLE after restart 1, no "no live owner" refusal, no error row); scenario 2 PASS (chat B answered BANANA after restart 1); scenario 3 PASS (chat A answered PINEAPPLE again after restart 2).

Chain table from agent-sessions.json in the isolated profile:

Step Chat A (never sent before restart 1) Chat B (one turn before restart 1)
Created [created 01a0d652-…] [created 01a0d653-…], one turn sent
After restart 1, tab shown [created 01a0d657-… supersedes 01a0d652-…] (one link), live fence 3 [created, resumed] same thread 01a0d653, live fence 3
After restart 2, tab shown [created 01a0d657 (supersedes …), resumed 01a0d657], live fence 5 [created, resumed, resumed] same thread, live fence 5

Nothing unexpected: model and options unchanged across restarts, exactly the 2 created chats (no duplicates), no error toasts, no stale thread ids shown, no refusal at any send.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready to merge

Head reviewed: ef6fb34cbf. No code changes were needed in this review.

Review loop. One independent review pass came back clean. It checked:

  • The user's issue is fixed end to end.
  • Every option chosen on an unsent chat carries over to the new thread: model, effort, fast mode, approval policy, and working folder. Per-turn options are re-sent on every turn, and the thread options go through the same start request used at create.
  • The fallback cannot fire over a saved conversation. That covers resumed, forked and imported chats, archived threads, and resumes by path.
  • Every reader of the handle chain sees only the new thread id after the replacement.
  • Mixed versions behave: a record written by this build reads as a normal one-link chain in an older build.
  • There is no added cost on launch, send, or record reads.

Architecture. The approach holds up. Whether Codex can resume a thread is decided by Codex's own answer about its own storage, and nothing in Orca stores a second copy of that fact. The fallback needs the exact error, for the exact thread, on a head Orca created. The main alternative is to start the Codex thread at the first send instead of at create, which removes this class of bug. It is a larger change, because the model picker and the lease both need a live session, so it stays a follow-up, as the description says.

Description corrections (no code change):

Live QA. An independent validation agent ran it live; see the comment above. The run used a hidden-window dev build of this head, an isolated profile, and the real Codex login.

  • A chat never sent to before a restart answers after the restart.
  • A chat with one turn resumes its own thread and remembers it.
  • After a second restart, the formerly unsent chat resumes normally.
  • The chain stayed one created link that names the thread it replaced, then created followed by resumed.
  • The replacement thread's saved conversation on disk holds both turns, and the unsent thread never had one.
  • Model and options were unchanged across restarts. There were no duplicate chats, error toasts, or refusals.

Readiness checks at the start and end of this review both passed:

  • 91 tests on the head passed.
  • The related suites pass on a test merge with current main. The one failure was a fake-timer lease-renewal test that failed under load and passed 3 of 3 times when run alone; this PR doesn't touch it.
  • Typecheck and lint are clean.
  • CI is green: 20 passed, 12 skipped, none failing.

Remaining gaps (not blocking):

  • Suppose a conversation's saved file is deleted outside Orca before the chat's first restart. Orca then starts a fresh thread instead of showing an error, as listed under Known limit. Nothing recoverable is lost.
  • Not run live on Windows, Linux, WSL, or an SSH-paired host. Structured Codex runs on the local host only, and the change has no platform branches.
  • The agent session history panel can briefly show an older message count until its periodic index refresh. The saved conversation on disk is complete. This lag exists on main and this PR doesn't cause it.

@brennanb2025
brennanb2025 merged commit eb746a6 into main Sep 25, 2026
32 checks passed
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