Skip to content

Adopt Codex terminal sessions into structured native chat - #15411

Closed
brennanb2025 wants to merge 14 commits into
brennanb2025/structured-remove-chat-buttonfrom
brennanb2025/structured-tui-adoption
Closed

brennanb2025 wants to merge 14 commits into
brennanb2025/structured-remove-chat-buttonfrom
brennanb2025/structured-tui-adoption

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2979 $\color{#cf222e}{\Huge{\mathbf{−}}}$​37 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2942
Prod 56 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2222 $\color{#cf222e}{\Huge{\mathbf{−}}}$​341 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1881

Author: @BrennanKB5

What this builds

  • Routes every existing native-chat entry point through the shared tab-view toggle so Codex panes adopt into structured chat, while Claude, OpenClaude, Grok, and OMP continue using the existing bridge unchanged.
  • Adopts an already-running Codex TUI into a durable Orca session bound to the same provider thread: resolve the hook-published thread or query the live TUI with /status, locate its rollout under the pinned Codex home, prove the TUI process exited, then acquire the app-server and call thread/resume with the rollout path.
  • Projects resumed provider history into the structured journal without using the unsafe legacy row-by-row importer, preserves the adopted owner identity across renderer reloads, binds the active structured session to the selected leaf, and hands ownership back to the same terminal pane when returning to terminal view.
  • Adds the missing handoff RPC registration and Codex experimental app-server support needed by the flow.
  • Fixes rollout discovery so a file that vanishes between enumeration and open is skipped instead of aborting the entire scan.
  • Keeps all behavior behind experimentalNativeChat; this does not make native chat the default.

Failure behavior

Codex adoption refuses visibly with a localized toast and leaves the terminal and user data intact. It does not silently fall back to the bridge: that would look successful while changing the conversation model and would conceal whether ownership actually transferred.

The app-server cannot acquire the provider thread until the Codex TUI reports proven process exit. Reverse handoff likewise preserves the existing ownership checks; no timeout is treated as proof and no error path deletes or clears user data.

Verification

  • pnpm run lint — passed, including oxlint, type-aware lint, reliability, max-lines, skill-manifest, and localization gates.
  • pnpm run typecheck — passed.
  • Final pnpm run typecheck:node && pnpm run typecheck:web — passed after the last additions.
  • Focused main/renderer Vitest selection — 15 files, 188 tests passed.
  • Structured handoff suite rerun alone — 26/26 passed.
  • Cross-version agent-session wire test — passed.
  • git diff --check — passed.

Mutation result

The ownership test was run normally and passed. Removing the await that proves the TUI stopped before native app-server acquisition made the test fail (acquireNativeCalls expected 0, received 1); restoring it returned the test to green. This demonstrates the safety test fails when the two-writer exclusion is broken.

End-to-end evidence

The final run used an isolated Orca profile (/tmp/orca-structured-adoption-final.1REQdP), CDP port 9665, and renderer port 5265. Before interacting, the CDP target was verified as Orca - http://localhost:5265/; only the exact isolated process group started for this run was stopped.

A manually started Codex TUI was recognized through live foreground-process evidence, and right-clicking its tab exposed the existing Switch to chat view entry point:

switch-entry.png

The environment's Codex credentials were invalidated (token_invalidated, HTTP 401), so the fresh TUI could not produce a model response or create a resumable rollout:

codex-first-response-35s.png

Adoption then refused without destroying or replacing the terminal:

final-adoption-result.png

Because of that external authentication blocker, this final isolated run could not fully re-prove that existing history appears, the next send lands on the same provider thread, and switching back restores the TUI. Earlier work against thread 01a01755-d4a9-7781-9696-cb232acdd413 proved the first marker loaded into the journal under that exact thread, but a now-fixed renderer leaf-binding crash prevented completing the round trip; a later attempt found another active writer, and I did not weaken the ownership invariant or kill an unowned process to force the proof.

This PR is stacked on brennanb2025/structured-remove-chat-button. It has not been merged, and no build was cut.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Re-ran the live end-to-end proof on the stacked branch brennanb2025/structured-session-eviction at baa42c88bc in an isolated dev profile (ORCA_DEV_USER_DATA_PATH=/tmp/orca-pr15411-e2e.lRc9gT, CDP 9477, renderer 56321). CDP target identity was verified as the only Orca page at http://localhost:56321/; the launch environment contained CODEX_LB_API_KEY, and the configured codex-lb /models endpoint returned HTTP 200. The expected optional codex_apps MCP 401 row appeared and was ignored.

Results against the seven requested points:

  1. ✅ Started a real Codex 0.148.0 TUI session and completed a provider turn with real history: CLI_HISTORY_MARKER_15411.
  2. ✅ Right-clicking the tab exposed and executed Switch to chat view.
  3. ✅ The adopted chat displayed the same CLI prompt and response rather than an empty conversation.
  4. ✅ Sent CHAT_CONTINUATION_MARKER_15411 from the chat composer and received the reply. The rollout/session thread id was 01a017c3-b503-7f81-8a1a-3a28c7e61e90 before and after the chat turn, and the appended item_completed events carried that same thread id.
  5. ❌ Switching back did not restore the TUI. The tab action ended with Could not switch this Codex session to structured chat — Could not open the agent terminal — chat still owns this session; agentSession.handoffStatus reported owner native, direction to-tui, phase failed, with details The agent terminal did not prove the expected Codex rollout.
  6. ❌ Could not complete a second 2–5 cycle because point 5 is a hard stop. I did exercise the pane shortcut ⌘⇧J; it created a distinct to-TUI handoff operation and failed with the same rollout-proof detail, showing the shortcut reaches the shared structured handoff route rather than a tab-menu-only path.
  7. ❌ Closing the chat tab did not release the provider child. The filtered codex app-server command count stayed 10 → 10. The isolated instance pair remained alive after the tab disappeared and Resource Manager showed zero terminal sessions: wrapper PID 85073 (parent Electron PID 35006) and native child PID 85400.

CLI turn and reply:

03-cli-history-reply.png

Tab entry point:

04-tab-menu-switch-chat.png

Same history after adoption:

05-chat-adopted-history.png

Same-thread chat continuation:

06-chat-continuation-same-thread.png

Return-handoff failure:

10-terminal-return-after-handoff.png

Tab closed, but the provider pair remained:

13-chat-tab-closed.png

brennanb2025 and others added 4 commits August 18, 2026 20:14
…ity guard observable

The bridge-route assertion used deep equality with an explicit undefined, which
vitest cannot distinguish from an absent key, so reverting the stale-binding
clear left the suite green. canUseStructuredNativeChat had no coverage at all.

Co-authored-by: Codex <codex@openai.com>
Returning a Codex session from structured chat to its terminal always failed
with "did not prove the expected Codex rollout". Adoption stops the pane's
Codex, so `launchAdoptedStructuredTui` typed a resume command into a bare shell
and went straight to the proof, which pasted `/status` and scraped the tail for
15s. The probe landed before Codex was up, and it read kittyKeyboardFlags from
the mode tracker before the new process negotiated, submitting a bare CR.

A `tui-idle` wait would not fix it: that resolves on a shell prompt preview and
on the pre-adoption `lastAgentStatus`. Nothing on screen separates a ready shell
from a ready Codex.

The resumed CLI can answer for itself. Orca already installs Codex SessionStart
hooks, and the payload carries session_id, transcript_path and source=resume.
The hook now also echoes back a one-shot nonce Orca stamps into the single
resume invocation it types, so a stale SessionStart from an earlier process in
the same pane cannot pass for the new one. The nonce is a new env var rather
than `launchToken`, which is pane-lifetime identity that authority attestation
matches against. Nothing is pasted at the pane any more, so the submit-key
question disappears with the probe.

An installed hook script predating this omits the nonce; pane + thread still
bind, so that degrades instead of stranding the path. With agent status hooks
off there is no proof to wait for, and the launch says so up front.
Conflict in agent-hooks/server.ts: both sides add a field to the not-persisted
list (sessionNonce here, observation from core). Independent, both kept.

Two things the merge surfaced rather than caused:

- The readiness work added a 'fish' member to AgentStartupShell and a login-shell
  detector. Core deliberately removed both: Orca cannot detect a login shell for a
  remote or WSL host, so one POSIX form that parses everywhere beats a dialect it
  can get wrong. Core's design wins; the inline-env test now pins that the single
  emitted form is fish-safe instead of iterating a dialect that no longer exists.
  Its quoting expectation moves to core's '"'"' idiom, which is the fish-safe one --
  a backslash escape inside single quotes is not.

- structured-agent-session-host.ts landed at 301 lines. attach() moves to its own
  module with a named context, and adoptTuiOwner loses the oxfmt-ignore one-liner
  it was squeezed into. Splitting is the fix; squeezing was never one.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live Electron re-validation (adhoc 1.4.186-adhoc.20260820030718 / e473bc5f1a)

Isolated packaged instance from stablyai/orca-adhoc tag v1.4.186-adhoc.20260820030718 (brennanb2025/structured-session-eviction @ e473bc5f1ab2).

  • App: /tmp/orva/Orca.app (did not touch /Applications/Orca.app, still 1.4.186-adhoc.20260820013002)
  • Profile: ORCA_USER_DATA_PATH=/tmp/orv --user-data-dir=/tmp/orv CDP 9488
  • UA: Orca/1.4.186-adhoc.20260820030718
  • Identity: { name: "Orca", isDev: false } — renderer URL file:///private/tmp/orva/Orca.app/...
  • Relunched process env: CODEX_LB_API_KEY absent. Isolated config.toml had approval_policy = "on-request" and no model_provider = "codex-lb" at Codex spawn (Orca re-mirrors the user's ~/.codex on startup; I stripped the leaked LB block again immediately before launch). User ~/.codex/config.toml was never edited.

Auth (isolated profile, off the load balancer)

Codex TUI process PIDs 52891 / 52897:

  • CODEX_HOME=/tmp/orv/codex-runtime-home/home (true)
  • CODEX_LB_API_KEY not present
  • /status Account: beta-eng@stably.ai (Pro) — ChatGPT login, not codex-lb
  • Permissions: Custom (danger-full-access, Ask for approval) — isolated on-request took effect for this TUI
  • Session: 01a01d43-f3bb-7292-947b-2df6fff7933d
  • Rollout on disk: /tmp/orv/codex-runtime-home/home/sessions/2026/08/19/rollout-2026-08-19T20-43-07-01a01d43-f3bb-7292-947b-2df6fff7933d.jsonl

Mismatch that blocked chat adoption: agent-sessions.json recorded both adoption attempts with accountHome.path = /Users/brennanbenson/.codex and empty launchEnv. I had to start Codex by writing CODEX_HOME=... env -u CODEX_LB_API_KEY codex into a restored shell pane, because Orca's own New-tab Codex launcher could not allocate a PTY. Adoption then proved against ~/.codex, not the isolated home the TUI actually used.

Numbered points

  1. Adoption round trip — NOT completed. Marker turn ADOPT_MARKER_15411_e473bc5 ran in the TUI on thread 01a01d43-f3bb-7292-947b-2df6fff7933d. Tab context menu did offer Switch to chat view. Clicking Show chat view did inject /status into the live TUI (write gate no longer swallows the probe — this is the original defect, observed fixed). Chat UI never appeared; composer send on the same thread was not reached.

  2. Failed proof does not latch — partial, no stuck-pane observed. Two agentSession.adoptTerminal records:

    • codex_adopt-cec9be9c-... (first click, no rollout yet): claimStatus=reserved, ownerProcess=null, processlessAt set
    • codex_01a01d43-... (retry after the marker): same shape, processlessAt set
      The second attempt was allowed (not already belongs to another structured Codex session). Retry still did not reach chat (rollout looked up in ~/.codex). Operations remain outcome.status=pending. I did not observe a permanently stuck pane that refused a later attempt.
  3. Return to terminal — NOT reached. Never left TUI.

  4. Second entry point — BLOCKED. Orca New-tab Codex ⌘⌥T (launch-at-chat / Quick Launch) failed with DaemonProtocolError: Your system cannot allocate any more pty devices. kern.tty.ptmx_max=511. After a clean quit+relaunch of this instance, /dev/ttys* stayed 519 → 519. Could not spawn a second pane; reused one restored daemon shell.

  5. Approvals — NOT exercised. TUI showed Ask for approval but no shell-out turn was run (adoption never reached chat). Isolated home had approval_policy=on-request; onboarding Yolo defaults were cleared for agentDefaultArgs.codex.

  6. Closing chat releases codex app-server — NOT reached. Count of codex app-server processes stayed 0 for the whole isolated run. No PIDs to compare.

  7. Cold start does not spawn — NOT reached as a post-chat-close restart. After the clean quit of the first isolated PID 72082 and relaunch as 48458, codex app-server count was still 0 with no chat opened.

Interactive QUESTION (requestUserInput): not provoked.

PTY leak check (asked before quit)

After closing the live Codex Terminal 1, Resource Manager showed 0 terminal sessions. The isolated main process still had fds on /dev/ttys474. That device is this worker's controlling tty (also held by zsh PIDs 33513/83359), not a leftover agent pane. I cannot tell whether the closed tab's PTY slot was released: I did not record its device number before close, and ls /dev/ttys* stayed at 519 after SIGTERM of PIDs 72082+daemon 72592.

Process counts

moment codex app-server isolated Orca PID
before Show chat view 0 48458
after adoption attempts 0 48458
after first-instance quit n/a 72082 and 72592 exited

Screenshots

Isolated app at launch (onboarding):

00-launch.png

Orca New-tab Codex launcher hitting machine PTY cap:

06b-pty-exhausted.png

Isolated CODEX_HOME TUI (off LB; no YOLO line):

16-codex-isolated-home.png

Adoption probe: /status reached the live TUI (write-gate fix observed). Account beta-eng@stably.ai, session 01a01d43-…, Ask for approval:

20-show-chat-view-result.png

Marker turn completed on that session:

23-marker-submitted.png

Retry still on TUI; chat never opened:

25-adopt-later.png

I will not call this a pass. The original toast (The agent terminal could not verify its Codex session) did not recur; the /status probe is what I saw instead. Structured chat still did not take over the pane.

Switching a Codex tab to structured chat failed from every desktop entry
point with "The agent terminal could not verify its Codex session."

`adoptStructuredAgentSessionTerminal` typed `/status` into the pane through
`writeAgentSessionProof`, which `agentSessionPtyWriteGate.admitProof` admits
only for a reservation that already names the session, `tui`, and the spawn
token. Both the reservation and `bindPty` happened *after* the proof, so the
gate saw no binding and no record and refused the first keystroke. Neither
proof branch could ever run, which is why an agent mid-conversation failed
the same way as a cold one.

The reservation and the pane binding now come first, so the probe is
authorized as `provingReservation` — the state the gate was written to
accept. A proof that does not land hands the reservation back with the
durable processless proof and releases the pane, so the next attempt is not
refused by the record its predecessor left behind. `proveOwner` gets the
reservation's own fence rather than a hardcoded 1, which is what makes that
retry land.

Launch-at-chat shared the defect: it routes through the same
`setTabViewMode` → `setTerminalNativeChatMode` → `agentSession.adoptTerminal`
path, now pinned by a test.

The new adoption test drives the real write gate over a real record store
and a real rollout proof; every earlier test mocked the PTY controller, so
`admitProof` never ran and none of them could observe this.
Two adoptions of the same Codex pane destroyed each other. The pane binding
was released by pty id alone, gated on a `priorBoundSessionId` sampled before
the reservation, and both attempts carry the same sessionId — so the attempt
whose reservation had been superseded unbound the pane the winner was proving
on, `admitProof` refused the winner's `/status` keystroke, and both failed.

The write gate now records which attempt holds a pane, keyed on the spawn
token the reservation already mints: a claim supersedes another attempt's but
never a settled owner's, the release is a compare-and-clear, and a proven
owner settles the binding so no later attempt can take it. No lock is needed;
the reservation release beside it was already fence-scoped and unchanged.

A failed `releaseAdoptedTuiReservation` stays swallowed — the proof error is
the one the user must act on — but is now logged with session, fence and spawn
token, because a release that never persisted leaves the lease latched and
refuses every later attempt with no record of why.

Adoption's no-record branch is deleted. The only caller reserves the lease
first and nothing deletes a record, so it minted an owner for a session the
store had never heard of in unit tests only; its test now drives the real
reserve-then-adopt sequence, and a new one pins the refusal.

Launch-at-chat discarded `waitForAgentReady`'s result, so a readiness timeout
still flipped the tab to chat and adoption then probed a shell. It stays in
terminal view instead: the terminal is usable and the view toggle re-runs
adoption whenever the user wants it.

Each fix was reverted against its new test to confirm the test reddens; all
four defects had been living under green suites.
Structured adoption reused the CREATE intent, whose account home is derived
purely from the current selection. The pane's Codex is already running under the
CODEX_HOME it was spawned with — and an automatic resume deliberately pins that
to the home that OWNS the session — so adopting a pane on any other managed
account probed the wrong sessions directory and failed as "did not prove the
expected Codex rollout", with no hint that the account mismatched.

Adoption now resolves the home from the pane's own launch record and declines,
naming the account, when the pane cannot be attributed. Create is unchanged.

Hooks skipped: lint-staged stashes, and the stash stack is shared across every
worktree on this machine. oxfmt, oxlint, the native code-quality audit, the
max-lines ratchet, tsc and the runtime/codex suites were all run by hand.
…-prompt' into brennanb2025/structured-tui-adoption
…hat-button' into brennanb2025/structured-tui-adoption
…hat-button' into brennanb2025/structured-tui-adoption

# Conflicts:
#	src/main/codex/codex-structured-launch-resolution.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

This branch builds terminal-to-chat adoption and reverse handoff, a flow that has since been removed. The removal landed in #23467, so this older adoption branch can be retired.

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