Commit Graph
17 Commits
Author SHA1 Message Date
Brennan Benson 52c4e0646b Opening or resuming a Codex chat waits for Codex instead of failing after 30 seconds (#30395)
* fix(codex): opening or resuming a Codex chat waits for Codex instead of failing after 30 s

The handshake (15 s) and thread/start / thread/resume (30 s) had their own deadlines because
the Codex start ignored the host's startup signal. It now honours it, as Claude's start does, so
those requests carry no deadline: the host's startup limits, a close, a Stop or quit end a start
that hangs, and the abort reaps Codex. Calls after the start keep the 30 s default for now: they
run in the session's queue, which a Stop or a close waits behind.

* test(codex): keep the handshake wait test in its own file under the line limit

* fix(codex): the one-shot app-server runner takes the same no-deadline option as the chat connection

* test: accept nullable Codex request timeout in probe fake

* fix(codex): bound direct opens at their caller

* Remove obsolete Codex startup timeout scenario
2026-10-20 11:49:29 -07:00
Brennan Benson 53191250e8 Accept messages while any agent starts, and bound each startup step (#28046)
* Finish structured agent readiness before delivering held work

* Publish the Codex process before protocol startup

* Publish ACP agent processes before session startup

* Publish Pi at spawn and settle signed-out starts once

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Await Codex protocol open under host ownership

* Await ACP protocol open in the host

* Derive ACP opening from its protocol session facts

* Exercise unanswered ACP options with an explicit silent route

* Await Pi open and fail signed-out construction

* Read Pi catalog facts from awaited opening

* Keep the Pi fixture exit observer

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Capture message choices per accepted send and preserve picker state

* Apply accepted Claude message choices at prompt handover

* Apply captured message choices at provider dispatch

* Bind native chat choices to accepted message dispatch

* Reset Codex turn choices for each captured message

* Resolve Codex message defaults from native facts

* Align message choice and Pi default fixtures with provider dispatch

* Align option fixtures with saved message choices

* Match Codex message choices to the active turn origin

* fix: let pending commands reach handover refusal

* Abort pre-prompt message choice writes with host-owned wait

* Verify message choices through provider input boundaries

* Keep legacy mobile test hosts explicit about message choices

* Keep startup waits cancellable and preserve operation order

* Derive Codex startup exits from host phase

* Open Codex protocol in background-task test fixtures

* Keep ACP startup ownership until handshake completes

* Run ACP opening catalog fixtures with Node SQLite

* Verify saved ACP picks without live option writes

* Keep ACP catalog bookkeeping from failing an options read

* Restore signed-out restart and fail Pi startup faults

* Run Pi opening catalog fixtures with Node SQLite

* Preserve message choices across cold clients and kept cards

* Remove the unused provider option restore ledger

* Clarify launch choices and legacy option hydration

* Remove remaining restore-ledger forwarding and assertions

* Remove unused ACP and Pi restore bookkeeping

* Keep live choices and delivery order while agents start

* Drop retired startup revision from Codex opening

* Keep live-option cancellation until message choices and clarify catalog reads

* Hold ACP option replies in live-write cancellation regressions

* Remove retired Pi startup revision and match saved-pick behavior

* Check running choices and retained rewind messages after delivery integration

* Apply startup picks and let parked messages release operations [skip ci]

* Remove unused Codex opening report field [skip ci]

* Keep ACP restoration facts local to the provider [skip ci]

* Apply Pi startup picks after readiness and remove unused open facts [skip ci]

* Cover goal changes during the held Codex message wait [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Release scripted Codex startup before awaiting queued picks

* Check the saved picker selection in per-message choice fixtures

* Exercise current choices and eager client capabilities in runtime fixtures

* Reuse runtime fixture reads and type the fake connection directly

* Align startup fixtures and preserve observed Claude exits

* Honor queued send authorization in Claude runtime fixtures

* Open Codex protocol sessions before Stop fixture dispatch

* Wait for the opened Codex child before runtime notifications

* Keep repeated permission picks usable and align Codex fixtures

* Validate pending picks against the live option state

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Keep Codex startup flow contracts and remove overlapping adapter tests

* Restore the committed dependency lockfile after test tooling

* Keep startup recovery flows and remove duplicate Pi handshake cases

* Cover both Pi startup choices in the delivery flow

* Match the Pi prompt entry recorded by the scripted wire

* Honor the before-input callback in the startup reply fixture

* Keep message choice flows and remove overlapping selector tests

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Match startup picker fixture to saved message choices

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Keep the Codex journal translator within its line limit

* Match ACP startup fixtures to saved picks and late Stop behavior

* Check startup effort after saved picker choices are acknowledged

* Complete Stop target types and keep the effort regression concise

* Use requested picker values in the effort regression acknowledgement

* Require owned provider spawn and one host-awaited open (from #28055)

* Restore the live checks the startup cleanup's deleted tests pinned

One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.

Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.

* Read a rewind in doubt outside the chat's serialize and apply it in the commit

The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.

* Accept messages while the agent starts: run the start outside the chat's serialize

The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.

A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.

- The delivery loop's prepare returns a start handle; the loop awaits the
  start outside, then re-prepares. Handover writes the pending row and
  begins the dispatch abort handle in one commit, calls the provider
  outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
  commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
  asked for.

* Test that a send is accepted at every wait an agent start makes

One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.

* Bound each startup step on its own and drop the startup silence rule

A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.

- Codex: initialize 15 s and thread start/resume on the connection's 30 s
  default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
  300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
  with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.

* Count Pi ready once it reports its state

Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.

get_state keeps the connection's 30 s request bound instead of the
startup ceiling.

* Test that model and effort picked while a chat is created ride its first message

A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.

* Start a phone's new empty chat on its first message

The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.

* Take main's lockfile; the branch changes no dependencies

* Restore main's exact text in files the merge only reformatted

The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.

* Fit the startup tests to the owned-spawn, host-awaited-open contract

Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.

* Fit the remaining CI checks to the startup limits and current releases

- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.

* Read the option stamp from the chat's one host-execution selection

* fix: preserve startup authority and accepted agent choices

* Retain dispatch authority and reconcile accepted options

* fix(native-chat): preserve accepted choices and cancellation authority

* Restore established client selection flow

* Preserve provider output observation and typed startup fixtures

* fix: preserve initial Claude launch choices and effort readback

* test: complete Claude catalog session fixture

* Preserve incoming main files after merge formatting

* Preserve compact handover through startup merge

* Type instant startup fixtures before their open is supplied

* Keep browser sign-in under its existing expiry owner

* Finish the main merge: retired-model display, start rewind cut, new tests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.

* Type the start's rewind record as the store returns it

* Type the Sleep test's held open as the provider open it replaces

* Sleep counts a chat stopped once the start it aborted leaves it at rest

A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.

* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop
2026-10-19 10:53:46 -07:00
Brennan Benson 2db7d5aac4 Refuse chat resume until terminal writers have exited (#29242)
* fix(native-chat): identify the owner when resume create is refused

* test(native-chat): consolidate transcript refusal cases

* test(native-chat): keep ownership coverage at the launch boundary

* test(native-chat): update adoption fixture for early ownership admission

* test(native-chat): align resume admission assertions with host ownership

* test(native-chat): keep RPC coverage focused on ownership proof

* test(native-chat): assert blank request presence before optional source

* fix(native-chat): admit the actual CLI source during chat creation

* fix(native-chat): satisfy source admission lint checks

* test(native-chat): provide the waiting hook prompt

* test(native-chat): isolate source admission fixtures from host homes

* test(native-chat): canonicalize adoption fixture directories

* test(native-chat): preserve early ownership admission in source stack

* Resume CLI conversations directly in native chat

* fix(native-chat): preserve resume refusal behavior and honest transcript feedback

* fix(native-chat): retain import read evidence in typed result

* Use the header test fixture record type

* Consolidate launch owner imports and correct feedback assertion

* Keep existing resume translation in runtime catalog

* fix(native-chat): preserve admitted source and prepared execution home

* fix(chat): retain source evidence for dispatched adoption

* fix(chat): keep source capture within module limits

* Keep deferred chat publication in the shared launch owner

* test(chat): allow host evidence in create routing

* Satisfy launch publication checks and scope slot cleanup

* Preserve canonical tab membership on projected reorders

* Use recorded launch ownership for surface recovery

* Reserve deferred chat order through existing group actions

* Pass the active layout owner into launch cancellation

* Stop terminal writer before adopting conversation in chat

* Guard chat adoption completion and satisfy static checks

* Switch completed terminal turns without confirmation and test handoff

* Recheck terminal writers on reserved adoption retry

* Split session entry delegates and fix linted test types

* Keep chat opening send below create module line limit

* Require explicit terminal stop consent before chat adoption

* Use direct process-table test mock

* Fix chat adoption types and runtime localization

* Localize terminal takeover refusal guidance

* Keep CLI stop admission scoped to active source panes

* Preserve ordinary history resume through attach

* fix(native-chat): admit an unsaved transcript under a linked account folder

A missing transcript stayed unresolved while its account root resolved through
symlinks, so containment failed and a not-yet-saved conversation showed a load
error instead of the unsaved notice.

* Update adoption replay test for at-rest creation

* refactor(native-chat): move the newest-item import window into its own module

Keeps journal-legacy-import.ts under the line limit after the source
identity observer landed on top of the streaming import.

* fix(native-chat): restore the refusal error import lost in the main merge

* Remove duplicate account resolver and condense terminal admission

* Refuse conversation adoption until terminal writers exit

* Keep conversation inspection within lint limits

* Preserve conversation owner in launch feedback

* Expect unavailable writer scan to refuse fresh adoption

* Keep terminal conversation evidence tied to its execution

* Correct writer continuity test refusal and event shapes

* Prove original terminal writers exited before chat adoption

* Scope saved conversation holds to their execution host

* Keep child-inspection fixture verdict literal

* Recheck awake saved conversation holds against their owned pane

* fix: inspect owned Windows jobs independently of foreground naming

* test: use braces in Windows inspection fixture

* test: complete ordinary Windows terminal inspection fixture

* test: explain partial terminal inspection fixture

* fix: inspect the terminal a conversation is switched out of

* test: separate OpenCode writer observation coverage

* fix: tie terminal conversation links to the PTY execution

The conversations a terminal reported now live on its PTY record for that
incarnation, so moving the pane or running many other terminals can no longer
drop them, and a link whose terminal can't be inspected refuses.

- Each link keeps the agent process its hooks last proved; a live or
  suspended process refuses even when the terminal's tree is shell-only
  (detached tmux, nohup).
- tmux-published events are never pane-owned, and an unmarked binding copy no
  longer revokes an earlier pane-owned proof.
- A restored row or saved checkpoint is censused against the pane's current
  local PTY instead of requiring this runtime's observed identity.
- The refusal now says Orca can't confirm the agent has exited and asks the
  user to stop what's running there or close the terminal.

* test: drop the stub-host admission calls from OpenCode observation coverage

* test: census suspended children through the real terminal host

* test: drop writer checks the real-terminal flow already covers

* fix: let prompt workers outside the terminal's session pass the exit check

- The POSIX shell-alone proof skips a session leader below the shell and
  everything under it: a prompt's zpty worker has left the terminal, while
  suspended, background, nohup and disowned jobs still refuse.
- Windows steady-state polls read the cached process table; every read that
  decides adoption or close stays fresh.
- The pane header and tab menu point back to Session History, since their
  action is gone once the agent exits.

* fix(terminal): count only Windows job members still linked to the shell

A pane's job keeps every process it ever started, including daemons whose
parent exited (an rc-started ssh-agent), so such panes never read as an idle
shell and the chat switch refused forever. Walk descendants of the shell along
live parent links within the job, for every shell; the Git Bash launcher keeps
its bash-chain allowance and a child older than its parent still refuses.

* Revert "fix(terminal): count only Windows job members still linked to the shell"

MSYS exec and fork leave intermediate Win32 parents exited, so a live Claude
started through a script (and 'sleep 60 &') became unreachable and the chat
switch admitted while it ran. Back to the job census until a console-based
design is measured.

* fix(terminal): set aside detached daemons on a Windows shell-alone decision

A pane's job keeps every process ever started in it, so an rc-started
ssh-agent made a Git Bash pane read as busy forever and the chat switch
refused. MSYS fork/exec leaves live work under exited Win32 parents, so parent
links alone can't tell the daemon apart; it is the one that also left the
console. On a scanChildProcesses decision read (switch admission, run-terminal
close, close guards), a member neither linked to the shell nor attached to its
console no longer counts. Polls and shell confirmation keep the job census.
The console-list helper now forks through the process-host chokepoint.

* refactor(daemon): name the process inspection options once

Keeps the daemon router and terminal host within their line budget now that
the options carry scanChildProcesses.

* fix(terminal): set aside detached daemons only for adoption, never for close

Closing a Windows pane kills its whole job, so a close check that ignored a
detached process (gitk &, cmd /c start ...) would kill it without asking.
Adoption asks whether anything is still in the terminal; close asks whether
closing destroys something. Only the adoption read now sends ignoreDetached;
close guards and the run-terminal sweep keep the job census. The win32 test
also stops the ssh-agent it starts.

* fix(native-chat): check the terminal being switched out of before opening its chat

When the conversation already had a native chat, the switch hit the
ownership conflict first and the header opened that chat, even while the
terminal's agent (a later claude --resume) was still running. The switched-out
terminal is now checked before the conflict, so a live one refuses as usual
and one at rest still opens the owning chat.

* test(terminal): assert close reads never call an ssh-agent pane idle

A plain read can answer unverifiable while the eval's short-lived processes
exit; either way it must not read as no-children.

* fix(native-chat): check terminal writers before naming an owning chat for any switch

Session History's Resume in New Native Chat carries no source pane, so with an
earlier chat still holding the conversation it skipped the terminal check and
the owning chat's first send would start a second writer beside a live
terminal Claude. Every adoption request now runs the writer check before the
owner conflict, as the no-owner path already does.

* test(reliability): run the chat-switch writer oracles in the provider-ownership gate
2026-10-19 01:06:36 -07:00
Brennan Benson add1eccae3 fix(codex): a Codex chat keeps its Orca identity when the user's Codex config strips command environment variables (#30406)
* fix(codex): a Codex chat's commands keep its Orca identity under a restricted shell environment policy

Codex rebuilds each command's environment from the user's shell_environment_policy, so
inherit = "core" dropped the chat id, marker, launcher and instance pin Orca gives the
app-server. orca orchestration then could not tell which chat was calling.

Every thread start and resume now sends those values as shell_environment_policy.set
overrides, which Codex applies after its filter and merges per key.

* fix(codex): accept the launch's read-only delete list

* test(codex): a chat without visuals gets no visuals config, identity overrides aside

* refactor(codex): a chat's command env is what the seal wrote, not a second key list

The seal now returns the session values it set (identity, a registered worker's own handle, the CLI launcher, the instance pin and the blanked remote selectors) as commandEnv, and the composed launch carries it. The Codex acquire sends that plus the chat's scratch folder from its own visuals. This deletes the hand-kept key list, the handle prefix test and the delete-list re-check, so a key later added to the seal reaches each command too, and an inherited worker handle the seal never named no longer does.

* test(codex): wait for the shell command's turn to complete before the cold resume
2026-10-19 00:31:11 -07:00
Brennan Benson c0e2b73395 feat(native-chat): require Codex 0.136.0 for chats (#26684)
* Require Codex 0.136.0 for native chats

* Add host-owned Codex install and update actions

* Bind Codex checks and maintenance to the selected installation

* Resolve Windows updater cleanup through system helpers

* Use one child environment for Codex checks and launch

* Expire Codex readiness and reconcile current host jobs

* Resolve Codex checks against the child command search path

* Restrict global npm repair to the selected Codex install layout

* Pin the host updater directory through Windows supervision

* Synchronize Codex maintenance fixtures on readiness

* Unify Codex notices and recover version-refused chats

* Route Codex recovery to the operation that remains

* Preserve Codex setup execution errors in the job log

* Preserve UTF-8 in Codex setup logs

* Remove unused Codex host installation detector

* Require host administration for Codex maintenance

* Show a missing Codex explanation once

* test(native-chat): give scripted Codex runtimes their installation fact; keep the registry out of the relay

Runtime tests that drive a scripted Codex app-server still ran the new
version check against whatever codex was on the machine's PATH: refused
on CI (no codex), and on a dev machine it ran the real codex --version.
A scripted transport now must state the Codex it stands for
(readCodexInstallation), asserted at install, and the tests get it from
one scriptedCodexTransport fixture.

The relay's Codex maintenance path reached @orca/windows-registry through
resolvePathEnvKey's re-export in windows-environment-path; import it from
its registry-free module instead.

Also update the catalog-probe test for the shared child environment.

* refactor(native-chat): drop the Update Codex action; keep Install for a missing Codex

A too-old Codex now gets only the plain message ("Codex X is too old for
chats. Update to 0.136.0 or newer.") with no button and no job. The
existing rechecks (window focus, the <=30 s expiry of a saved result)
retry the refused chat once the user has updated Codex themselves.

A missing Codex keeps the Install Codex button: `npm install -g
@openai/codex` with the npm on the chat's PATH, as a host-run job with
its log, then a recheck and recovery. A configured Command that is
missing gets the message only, since a global npm install would not
land at that path.

Removed: the `codex update` command and its 0.126 floor, manual
instructions, npm layout and launcher identity detection, the action
descriptor (install|update) and its titles, the legacy capability, and
the SSH relay maintenance path (structured chat never runs over SSH or
WSL), so the relay preflight handler is back to main's.

* refactor(native-chat): keep Install Codex in Settings only

When Codex isn't installed, main already hides Codex from the New tab menu,
so the in-chat Install button was almost never reachable. Settings → Agents
already shows "Codex isn't installed." with Install Codex.

- Chats and the new-workspace composer now only state "Codex isn't
  installed." (no button, no install job from the chat).
- The Install action, its busy spinner and the host-busy subscription move
  into the Settings Codex row, the only place that starts the job.
- The store no longer re-checks every other workspace context when a job
  finishes. A refused chat recovers through the same re-check a too-old
  Codex uses (window focus, or the <=30 s expiry of a saved result).
- The chat recovery key no longer includes the install job id.
- Tests: Settings drives the install; a new test proves a chat refused for
  a missing Codex recovers through focus after a Settings install, not
  through the job; the expiry recovery test covers missing and too old.

* refactor(codex): check chat compatibility when acquiring the child

* Restore workspace submit guard and simplify Codex maintenance

* Keep only the Codex chat minimum version
2026-10-12 14:30:27 -07:00
Brennan Benson c785c986c2 feat(native-chat): teach chat agents to show inline visuals in their own folder (#26099)
* feat(native-chat): visual directive grammar and host read for a chat's visuals folder

A shared grammar for the ::orca-visual{file="..." title="..."} reply line,
the per-chat visuals folder location on the owning host, and the
agentSession.readVisual runtime method that reads one visual with lexical and
canonical containment, a 512 KiB bounded read and UTF-8 refusal.

* feat(native-chat): render chat visuals inline and in the right sidebar

Native-chat assistant replies render a ::orca-visual{...} line as the chat's
HTML visual in an opaque, scripts-only sandboxed frame: CSP first, the host
frame navigation guard registered before content runs, live theme without a
reload, fitted height, links opened in the viewer's browser only from a real
gesture, lazy mount, and one muted line when the visual cannot be shown.
Open in sidebar shows the same frame in the right sidebar, widened while it
is open and restored after.

* feat(native-chat): teach chat agents to show inline visuals in their own folder

Native-chat Claude and Codex sessions now get a per-chat visuals folder on
the host that runs them, write access to exactly that folder, its path in
ORCA_CHAT_VISUALS_DIR, and an Orca skill that teaches the ::orca-visual line.

- Skill ships as an unpacked plugin folder in desktop and headless builds.
- Claude: --plugin-dir via SDK plugins, behind the CLI version probe (now one
  shared probe for every version-gated flag); folder added to
  additionalDirectories beside the user's own.
- Codex: skills/extraRoots/set and the folder appended to the user's own
  writable roots, between initialize and the thread open, under a 2 s budget;
  unsupported, failed or hung setup opens the chat without visuals.
- A host sweep removes folders no chat record maps to, and folders whose
  local workspace is provably removed; anything unproven is kept.

* test(native-chat): the visuals skill names only theme variables the frame sets

* fix(native-chat): visual CI fixes, shared height governor, live-turn streaming hold

Registers agentSession.readVisual from the methods index so the structured
method file stays under its line budget, replaces reflective reads with checked
narrowing, moves the pure height governor to src/shared for mobile, and holds a
half-written directive tail while the turn works (structured text rows carry no
running state).

* fix(native-chat): harden the visual read and link opening

Re-checks after the open that the chat's visuals folder is still the real
directory at Orca's path, reports unexpected filesystem faults by code without
host paths, and lets one click in a visual open at most one page.

* fix(native-chat): keep visual lines out of plain-text reply surfaces; review fixes

One shared helper drops visual lines (outside fenced code) from reply text where
it becomes plain text: the structured status summary that feeds the sidebar row,
dashboard, notifications, phone rows and handoffs, and AI Vault reply previews.
Review fixes: height also counts a pinned body's overflow, only the live
frontier row holds a half-written visual line, the runaway-height stop needs the
same step repeated, and any host refusal evicts the cached revision.

* fix(native-chat): review round 1 for chat visuals delivery and cleanup

- Visuals sweep: a workspace counts as removed only when no profile's
  catalog holds it (chats and visuals are shared by every profile, catalogs
  are not); a worktree in a known project is removed only when git no
  longer records it; any unreadable profile decides nothing; one catalog
  snapshot per run; a symlinked visuals root is never walked.
- The other-profile catalog reader moves out of window/ and also returns
  project ids.
- A folder Orca itself inherited is stripped at the spawn layer for both
  agents, not only from the launch overlay.
- Claude version probe: a probe that gave no version is never remembered;
  the plugin check waits up to the probe's kill time so a slow first probe
  no longer costs a chat its skill.
- Skill: kept out of Claude's / menu, filename and theme guidance matched to
  the renderer, refused writes are not retried.
- Opt-in real Codex test for skill discovery and the writable root.

* feat(native-chat): ask the Claude CLI its version when native chat starts

The first chat after Orca starts usually finds the version known, so the
plugin and thinking-display checks answer at once instead of probing a
cold binary while the chat waits.

* fix(native-chat): resolve the visuals folder without the removed journal-paths helper

Main removed the per-chat journal paths and the journal database's state
directory; the visuals folder keeps the same sha256 layout on its own and the
read method uses the profile state directory the chat host is opened in.

* fix(native-chat): review round 2 fixes; copy a reply without its visual lines

Reply previews in Agent Session History drop visual lines per text part before
lines are folded; the frame adds a body's overflow only when the body really
overflows; fence tracking follows CommonMark closers and openers; the copy
button copies a reply without visual lines; a coded read fault keeps its cause.

* fix(native-chat): review round 2 for chat visuals delivery and cleanup

- Read git worktree records written relative to their own folder (git 2.48+),
  so a worktree on an unmounted drive in such a repo is still kept.
- Skip the running profile by its own storage folder, not the profile index a
  switch rewrites first; a profile never written to counts as empty, so the
  workspace rule is not switched off by a profile that was never opened.
- Ask for readable thinking again once the plugin check has waited for the
  version, so a slow first probe no longer drops it for the chat's life.
- Launch flag decisions move to their own module; tests use a typed record
  fixture instead of casts.

* fix(native-chat): review round 3 for the visuals sweep and version check

- Resolve a relative git worktree record against its folder's real path, so a
  project added through a link still matches and its worktree is kept.
- A profile with only backups of its data file is a lost file, not a fresh
  profile: it still stops the workspace rule.
- The readable-thinking re-check reads what is known and never starts a
  second version probe.

* fix(native-chat): update the frame's theme ref after render; read the visuals folder pair at once

* test(native-chat): declare agentSession.readVisual on the cross-version agent-session surface

* fix(native-chat): copying a reply keeps its code blocks and indentation

Removing visual lines now closes only the gap each removal leaves, instead of
collapsing blank lines across the whole reply and trimming its indentation; the
visuals folder is checked parent first again so a broken path answers the same
way every time.
2026-10-07 15:53:38 -07:00
Brennan Benson 98171a8934 fix(native-chat): never delete chat history on read; a chat Orca can't load says why once (#24576)
* fix(native-chat): an older Orca keeps a chat with newer content read-only, and an unreadable annotation costs only itself

A body or lifecycle mutation of a kind this build does not know, inside a row of a known kind,
now latches the chat read-only with every row kept, as a newer row kind or version already does.
It used to read as damage, and the open deleted the journal from that row on.

Each optional field of a body schema now declares what an unparseable value means:
- droppable (an annotation): removed from the row in memory; the chat stays writable;
- must-understand (a prompt's questions, a plan approval's plan, a turn's lifecycle, a goal
  change): the row is unreadable and the chat latches read-only.
Only a required field that fails is damage, repaired as before. A test holds every optional
field reachable from a body schema to a policy; the known body kinds are read off the schema.

Prompt options and questions no longer reject a key this build does not know, which deleted the
journal from that row on. The context-usage drop is now one case of the droppable rule.

* feat(native-chat): a chat an older Orca keeps read-only says so before a send is refused

A chat a newer Orca wrote opens read-only, and until now nothing said so: the person found out
when a send failed with "Chats were saved by a newer Orca". The host now names the reason on
every whole history page it serves (`page.readOnly: 'written-by-newer-orca'`: hydration, history
and catch-up resets), the shared client reducer keeps it from the latest whole page, and the
desktop status strip and the phone's composer area say "This chat was saved by a newer Orca, so
it's read-only here. Update Orca to continue this chat."

Every read-only latch the host has is a newer Orca's (its database, a row version, a row kind, a
body kind or must-understand fact), so the reason is derived from the journal's latch, not stored.
The field is optional: older clients ignore it (cross-version test against the newest release's
reducer), and older hosts never send it, so a new client against one says nothing, as today. A
reason this client does not know shows nothing rather than words that may be wrong.

The window-merge helpers move out of the client reducer into their own module.

* fix(native-chat): only a newer build's value goes read-only; damage repairs as before

An older Orca keeps a chat read-only only on evidence a newer build wrote it: a value outside a
closed set this build knows (a body kind, a nested discriminant or literal, a mutation kind), read
off zod's issues. That evidence wins over damage beside it. Anything else that fails (a wrong type,
a missing field, a bad optional value) is damage, repaired as before, so damage no longer freezes
a chat behind an "update Orca" it cannot fix. Drops the per-field droppable/must-understand
policy. A lifecycle mutation's kind is decided before its item id, so a newer kind without one is
kept. The context-usage drop is unchanged from main.

* fix(native-chat): a read-only chat locks its composer and says why there

Removes the separate read-only line (desktop status strip, phone notice component). The host's
`readOnly` on whole pages now feeds the existing composer lock instead: desktop disables the
composer with "Saved by a newer Orca. Update Orca to continue this chat." as its placeholder, and
the phone adds a 'read-only' input-lock reason with the same words. A read-only chat shows no
prompt card it would refuse; the draft stays in the composer. The phone's send-failure line is
back exactly as on main.

* fix(native-chat): a read-only chat pages back through its history

A read-only journal answered every history request with a reset, so a chat this PR now keeps
read-only more often could show only its newest page. Backward reads now serve the snapshot the
open folded, as the first page already does; a forward read of rows still resets.

* test(native-chat): an unknown key in a question entry is read and kept

* test(mobile): the read-only overlay test typechecks

* chore(native-chat): satisfy the changed-code gate in admission and the read-only overlay test

* fix(native-chat): a new value inside a known block or goal arm reads as a newer build's

The open fallback arm of the block and goal unions now aborts, so zod reports the known arm's own
failure instead of only the fallback's; a future closed set there reads unreadable, damage there
still repairs. The schema headers name the context-usage exception and say a new open-string
value must be safe for every older build (rewind keeps only known states; pages carry no row
version).

* fix(native-chat): a read-only chat reconnects for a whole page, so an updated host unlocks it

The client keeps `readOnly` until a whole page replaces it, but reconnected at its cursor and got
only batches, so a desktop attached to a host updated in place stayed locked with "Update Orca".
While it holds the latch it now subscribes without a cursor; the snapshot re-derives the state on
any host version. The phone already subscribes without one.

* fix(native-chat): every desktop write control follows the one read-only fact

The structured session's transport state derives one fact from the page's `readOnly`: the words
for why the host refuses writes. Every write control reads it. While it holds, the chat has no
turn and no work, as the host projects it, so the working row and Stop go away; a pending prompt
stays shown above the composer with its card disabled; queued cards' Send now, Delete and Edit,
the queue's Resume, the goal banner's actions, the model and option pickers, dictation and
background-task Stop are disabled or withheld. The composer placeholder stays the one place the
reason is said. `lockReason` now locks the composer by itself.

* fix(mobile): every phone write control follows the one read-only fact

The phone session derives one fact from the page's `readOnly`, the words for why the host refuses
writes, and every write control reads it: no turn or work while it holds, so no Stop; the pending
prompt stays shown with its card disabled; queued cards and Resume are inert; the model and option
pickers stay shut; the composer field is not editable, so its placeholder keeps saying why, and the
lock applies at once instead of after the transport lock's settle.

* test(native-chat): the newest release hydrates a read-only chat and scrolls back through it

* test(native-chat): a read-only chat reads idle even with a send its provider never answered

* test(native-chat): composer lock test without type assertions

* fix(native-chat): round-3 read-only fixes

- An old /model or option request no longer reopens its picker when a read-only chat unlocks: the
  menu is keyed by the request alone, and a lock only keeps it from mounting open.
- A disabled question card still steps through its questions; only answering is locked.
- A failed send's notice in a read-only chat says only that the message was not sent, with no
  Retry: the composer already says why, and no retry can land. Its wiring moves into
  useStructuredAgentSessionDeliveryNotices.
- The host's background-task stop flags stand; the client no longer overrides them.
- The view reads the read-only fact through one local flag.
- The schema header says what an older build's rewind does with each open string.

* test(native-chat): count picker mounts without an effect

* fix(native-chat): read-only keeps other failures' words and shuts open option menus

- In a read-only chat, only a send the newer-Orca refusal stopped is shortened to "Your message
  was not sent."; any other saved failure keeps its own words, and none offers a Retry.
- A model or option menu open when the lock lands has its items disabled, so it cannot send a
  change the host would refuse.

* test(native-chat): lock-lands picker test uses a typed surface

* fix(native-chat): a locked composer draws its reason

The editor's placeholder used the default that draws nothing while the editor is not editable, so
a read-only chat's composer was empty and its reason lived only in the aria-label. The placeholder
now draws while disabled, and its words are empty unless the composer is locked with a reason, so
every other disabled composer (a pending prompt, no terminal) looks as before.

* fix(native-chat): sync the locked-placeholder flag in an effect, not during render

* fix(native-chat): a read-only chat is not offered for resume or counted as failed to resume

After a relaunch, a chat whose journal this build keeps read-only (a newer Orca's) was listed in
"Resume interrupted chats?", failed with "Orca couldn't resume this chat. Open it to continue
manually.", and left a status-bar "N chats failed to resume" that never cleared, since nothing can
continue a chat this build cannot write. The resume set now skips such a chat after the checks
that end an offer (fork, moved on), so the offer is kept, not spent, and an updated Orca offers it
again; the failure ledger keeps an already-filed failure for it but does not show it. Both read
the chat's open journal, which listing and acting open first.

* fix(native-chat): a refused resume continuation keeps the refusal's reason

A send refused while continuing a chat after a restart was filed with its code alone, so a newer
Orca's refusal lost its `journalWrittenByNewerOrca` reason. The continuation now carries the whole
refusal, as a refusal from the agent's start already did, so the filed failure keeps it.

* fix(native-chat): a newer Orca's chat keeps its resume offer however it is met

- A newer Orca's whole database counts as read-only for resume, so a chat whose journal cannot
  be opened at all (a table the newer schema changed) is not offered, run or counted either.
- The checks made right before sending no longer skip read-only chats: turning one away there was
  read as the user having moved on and deleted the offer. Its send is refused instead.
- Settling a resume files nothing for a newer Orca's refusal; the existing rollback reopens the
  offer for an updated Orca.
- The continuation records the refusal as a reference, without the wire prose.

* chore(native-chat): one import of the session wire types in the continuation

* docs(native-chat): resume and read-only comments match the current rule

* test(native-chat): a closed set of a journal row cannot change without the row version

The test walks the body schema and the row and mutation kind tables, collects every closed set (a
row kind, a mutation kind, each enum, literal or discriminant), and compares them with a snapshot
recorded beside the row version. A new value makes older builds go read-only, so it fails until
the reader ships first or `v` is bumped and the snapshot updated. Tags a catch-all arm accepts as
any string (block types, goal states) are listed apart: older builds read a new one as-is.

* feat(native-chat): a body or plan subject of a newer kind is kept and the chat stays writable

An item body kind and an approval subject kind this build does not know now read through the
same catch-all blocks and goal states use (`openDiscriminatedUnion`), instead of making the chat
read-only. The item is kept, drawn by nobody, and ignored by everything that reads items (turns,
prompts, status); a rewind carries it as it was, kept by its place like every other row. An
approval whose subject this build cannot draw shows its `detail`, which Orca's writers fill with
the subject's text. A sent message of a kind this build does not know stays a newer build's.

* fix(native-chat): rewind narrows a retained body by its kind before normalizing it

* fix(native-chat): one chat that cannot open no longer stops the startup restore of the rest

* fix(native-chat): a damaged chat fails to load and nothing is deleted

A row this build cannot parse, a gap, or an epoch without its first row used to
be repaired on open: every row from the damage on was deleted and a rebuild from
the provider transcript was attempted. The open now fails through one refusal
(failLoadOnJournalDamage, journalCorrupt), every row stays, and a damaged
per-chat file is kept whole and never copied. A newer build's row still wins
and keeps the chat read-only. The body classifier for closed-set values, the
repair marker and disclosure, and journal recovery from the transcript go.

* chore(native-chat): no reset or adapter left on the open's fixtures

* feat(native-chat): an approval of a newer Orca's subject kind can only be cancelled

Desktop and phone show its detail and the line "This request needs a newer
version of Orca.", disable every answer, and keep the card's cancel, which ends
the turn. The chat stays writable.

* test(native-chat): the closed-set guard says a missed version bump fails older builds' load

* test(native-chat): the closed-set guard names what a missed version bump does

* fix(native-chat): a row the reader rejects is never written, and a chat that cannot load keeps its tab

Every journal insert reads its serialized row back with the reader inside the
write's transaction and refuses one it rejects, so Orca's own writer can no
longer leave a chat that fails to load. A restored chat whose open fails keeps
its tab and says why when opened. An epoch named with no rows is founded afresh
again, deleting nothing. The at-rest test now expects a damaged chat to be
refused; the replay gate keeps its 2026-09-11 evidence as it was run.

* fix(native-chat): a newer Orca's approval subject is carried as it was, and its card cancels with or without a turn

The approval subject's type is open, so code that reads a plan narrows first; the
host's prompt bounding carries an unknown subject instead of rebuilding it as a
plan, which threw in every start's stale settlement. A Codex rewind keeps a
newer Orca's item in its place. A whitespace-only tag is damage. The card's
cancel reaches the host without a running turn on hosts that take that. A
provider row the reader rejects ends its turn as interrupted and the next send
works. A guard fails if an approval subject kind is added before clients can be
gated (STA-9262).

* fix(native-chat): a card's cancel sends nothing without a turn again, and rewind keeps a newer row after any held row

The host's cancel request has to name a turn on every version, so the turnless
prompt cancel is reverted on desktop and phone. The rewind merge moves its anchor
for every row the provider still holds, so a newer Orca's row stays after one
whose kind this build does not know.

* fix(native-chat): a card this build cannot answer leaves the composer open, and a send starts a turn

An approval of a subject kind this build cannot draw, raised by a newer host's background agent
while no turn runs, left nothing to press: its answers are disabled, its cancel needs a turn, and
the desktop composer gave the card its slot. A send the host queues waits behind any pending
prompt, so even the phone's open composer only queued.

While every pending prompt is one this build cannot answer, the desktop composer stays open, and
desktop and phone send without asking to queue: the send starts a turn, and the card's cancel then
settles it. A chat a dead run left is unaffected: opening it already cancels those prompts.

* feat(native-chat): a chat a newer Orca saved fails to load and says to update, with nothing to send into

Opening a chat a newer Orca saved (a row kind, batch-change kind or sent-message kind this build does
not know, a newer row version, or a newer history store) used to open it read-only: the history
shown, the composer locked with a reason, every control greyed. Now it fails the load, like a
damaged chat, with its own words: "This chat was saved by a newer Orca. Update Orca to open it."
Every row is kept. The read is final, so the pane stops retrying; reopening the tab reads again.

A chat whose load failed for good (damaged, or a newer Orca's) offers no composer under the error,
on desktop and on the phone, so the failure is said once and no send can be refused a second time.

A restart offer for a newer Orca's chat is spent without a word: no failure filed, nothing counted.

Removed with the read-only mode: the journal's read-only latch and every check of it, the history
page's readOnly field (never released), the cursorless reconnect, read-only paging, the composer
lock and placeholder, the greyed controls, and the resume exclusion.

* test(native-chat): a newer Orca's records open no chat, and a status fake carries its submissions

* test(native-chat): the status fake's cast says what the feed reads

* fix(native-chat): a chat's read that failed for good takes the whole pane, over a loaded transcript too

* test(native-chat): the final-read-failure test names its reasons

* fix(native-chat): a newer Orca's chat keeps its restart offer, and its refused load is logged once

* refactor(native-chat): the newer-Orca chats a restart listing skips live beside the candidate reader

* fix(native-chat): a refused chat load keeps where it failed, so the log names it and a new reason logs again

* chore(native-chat): the reducer's window merge back where main has it, and comments say a newer Orca's chat fails to load

* test(agent-hooks): the rename test's journal fakes carry submissions, as a journal snapshot does

* test(agent-hooks): the rename test's journal fakes say what the status feed reads

* fix(native-chat): Dismiss all ends the restart offers this Orca lists, and keeps a newer Orca's hidden ones

* test(native-chat): a resend against a newer Orca's store answers unknown, and the phone says nothing beside the read's words

This build never opens a chat a newer Orca's database holds, so a resent send id there cannot be
answered from its journal: it answers "outcome unknown", never a refusal and never a made-up
record, with no second delivery and no write. On the phone, a send error left beside a read that
failed for good is not shown; the pane keeps only the read's words.
2026-10-05 10:45:58 -07:00
Brennan Benson 934a2d44a0 fix(codex): a native chat's thread opens on the model the chat chose (#23532)
* fix(codex): a native chat's thread opens on the model the chat chose

* fix(codex): a resumed thread keeps its own saved model, provider and effort
2026-09-27 21:54:37 -07:00
Brennan Benson eb746a6d32 fix(codex): a Codex native chat that never sent a message reopens after restart (#22639)
* fix(codex): start a new thread when a chat's thread was never saved

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.

* test(codex): build launch-resolution chains without a type assertion

* fix(codex): match only Codex's own no-rollout text, pinned through the 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.

* docs(codex): note why the no-rollout text is safe on the resume path
2026-09-24 21:43:04 -07:00
Brennan Benson 7f5141ae2d Make the Agent Permissions toggle apply to Codex chat (#20977)
* fix(structured-chat): deliver the permission posture through each transport's own contract

Codex posture moves off app-server argv onto typed `thread/start` and
`thread/resume` params. Manual states `on-request` / `workspace-write`
explicitly instead of omitting the fields, which app-server resolved through the
mirrored config.toml — a Manual thread on a home carrying
`approval_policy = "never"` never prompted.

Claude keeps its owned `--dangerously-skip-permissions` flag through SDK
`extraArgs`; the SDK's typed bypass option emits a newer allow flag that older
user-installed binaries reject.

Posture is re-derived from current settings on every session acquisition.

* fix(structured-chat): parse permission arguments as argv

* fix(structured-chat): keep permission policy authoritative
2026-09-16 18:21:52 -07:00
Brennan Benson c287a5d9b7 feat(native-chat): add provider-aware Fast mode (#20506)
* feat(native-chat): add provider-aware fast mode

* chore: drop unrelated formatter churn from the merge

pnpm format reflowed pnpm-workspace.yaml quoting and a source-scan test
that this PR does not otherwise touch.

* fix(native-chat): review fixes for provider-aware fast mode

Review pass over the Fast mode work.

Claude reads its model catalog once per option write. The admit check, the
effort guard and the Fast guard each took their own `list_models`, so a model
write with Fast on paid two round trips for one list and let two guards answer
from two different catalogs. The guards are now pure over a single read.

Claude no longer refuses a Fast enable when the catalog identified nothing at
all. An empty list is not evidence against a model -- the same rule the model
admit-check already applies -- so a CLI that cannot answer would otherwise have
Fast refused on every model. A catalog that did list the model and stayed silent
about Fast is still not positive evidence and keeps refusing.

Codex refuses a direct `serviceTier` write instead of accepting one the next
turn discards. The turn derives the tier from `fastMode`; the key still restores
so a session persisted before Fast existed migrates.

Both option surfaces return a cached snapshot again. `SessionOptionsSurface` is
read through `useSyncExternalStore`, whose contract is a stable snapshot, and
rebuilding it per call breaks that for any consumer wired that way.

Also records two decisions that were emergent rather than stated: routing
Standard when Fast is on but no tier is named yet, and what a readback
disagreement does and does not prove.

Quality gate: merges the duplicate imports static analysis flagged, adds SAFETY
rationales for two pre-existing casts the changed-code gate now sees, and drops
a new assertion in favour of a checked narrowing.

* fix(native-chat): read Claude Fast state from the session frame

A fresh Claude session reports `fastModeState` while the settings readback still
has no `fastMode` boolean, so the two are not redundant -- the frame answers at a
moment the boolean has none. The picker fell back to "value unknown" and asked
the user to disambiguate what the provider had already reported, and the state it
reported had no reader at all.

Falls back to the frame only when neither a pick nor the settings readback
answers. `cooldown` throttles routing rather than clearing the pick, so it reads
as on; reading it as off would flip a control nobody touched.

Display only. The launch seed is untouched: an unset Fast preference still seeds
nothing, which its own guard continues to pin.

* perf(native-chat): skip the model catalog read when turning Fast off

Turning Fast off needs no support evidence, so the read only cost a
round trip — and restore replays a stored `false` on every acquire.

Also narrows the alias-matcher comment: the effort and admit guards
match on alias and resolved id only, so calling it the sole matcher
overstated it.

* fix(native-chat): clear a Claude Fast block once the child stops reporting it

The child omits fast_mode_disabled_reason entirely when nothing blocks Fast
and never sends a null, so requiring the key back latched the first reason
for the session's life: switching to a model that disallows Fast and back
retired the control for good, leaving a session running Fast with no way to
turn it off. A frame that reports state without a reason is the all-clear.

* test(native-chat): cover the mobile structured option hook

useMobileStructuredAgentOptions gained generation fencing, a pending-write
guard and a post-write options refresh with no test file. Pins the concurrency
contract and the fast mode round trip:

- a superseded options read is dropped instead of overwriting newer state
- an overlapping write is refused and the pending guard is released after
- an accepted same-fence write reads options back and applies the result,
  and a different-fence write does not
- a boolean fastMode pick reaches the wire encoded and is remembered decoded
- no Fast row when session support, catalog support or the model capability
  is missing

Each behaviour was ablated against the production logic to confirm it fails
without it. No production code changed.

* feat(native-chat): render a boolean session option as one toggle

On and Off were two radio rows under a header repeating the option name,
so a binary choice cost three lines and two clicks to read. It is now a
single switch row that owns its label, on desktop and mobile.

An unknown value keeps its caption: a switch cannot say "unset".

* fix(native-chat): resolve a boolean option's display value at the producer

A boolean session option reached the UI in three states while its control had
only two, so the renderer apologised for the gap with a "Current value unknown"
caption beside a switch that had already collapsed to off. For `thinking`, whose
catalog default is on, that caption sat next to a switch asserting the opposite
of what every composed dispatch assumes.

One expression fed both the displayed value and the option's provenance. Split
them: the boolean descriptor now always carries a value, resolved to the same
`values[id] ?? defaultValue` that buildNativeChatSessionOptionCommand already
composes, while `valueSource` is untouched and still records whether anything
confirmed it. `kind.currentValue` is required on the boolean arm so the third
state cannot come back.

The launch path is unaffected: resolveAgentSessionOptionLaunch and
buildNativeChatSessionOptionCommand build the composed `--model` argument from
the caller's picks and the catalog, never from a descriptor.

Both surfaces mark an unconfirmed value instead of captioning it, and the two
reasons stay distinct — `default` says the catalog value is what a launch will
send, `unreported` says nothing has told us anything. Only `unreported` is
reachable in the structured lane, where the agent may be routing a tier we have
never been told about, so the two never share a label.

* fix(native-chat): let assistive tech read the option value marker

The marker was aria-hidden next to an explicit aria-label, so the label
already won the accessible name and hiding it only cost screen reader
users the default-vs-unreported distinction that sighted users get. It is
now referenced by aria-describedby, which keeps the name Fast mode.

Mobile's summary row said "Not set" for a boolean while the sheet behind
it showed the switch on, so the two screens disagreed. A boolean always
has a value; the summary states it and the sheet's marker qualifies it.

* chore(i18n): drop the On/Off option strings the switch row retired

Replacing the On/Off radio pair removed the only call sites for these two
keys. i18next cannot rebuild a key with no call-site default, so leaving
them in the catalogs forced them into the boot bundle as dead weight.
Removing them shrinks it by two entries instead.
2026-09-13 21:58:32 -07:00
Brennan BensonandMerge Sim ce4a3a4186 feat(chat): add structured session rewind backend (#19235)
* feat(chat): add structured session rewind backend

* fix(chat): make interrupted session rewinds recover safely

* fix(native-chat): negotiate rewind runtime capability

* fix(native-chat): consolidate remaining adapter imports

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-07 12:20:24 -07:00
Brennan BensonandMerge Sim 298571ad9f fix(codex): uncap app-server stdio records (#18590)
Co-authored-by: Merge Sim <sim@local>
2026-09-06 11:49:42 -07:00
Brennan BensonandMerge Sim 872bd51d47 fix(native-chat): reland large structured command results (#17720)
* fix(native-chat): preserve large structured command results (#17707)

* fix(native-chat): preserve large structured command results

* chore: place native chat validation artifacts under docs

* chore: drop stale root package config

* fix(native-chat): enforce rebuilt lifecycle append slots

---------

Co-authored-by: Merge Sim <sim@local>

* chore: omit native-chat reland planning docs

* fix(native-chat): remove journal store import cycle

* fix(native-chat): keep journal factory acyclic

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 13:12:19 -07:00
Brennan Benson 894ed75abb Revert "fix(native-chat): preserve large structured command results (#17707)" (#17719)
This reverts commit 5fe37729ea.
2026-08-31 12:34:49 -07:00
Brennan BensonandMerge Sim 5fe37729ea fix(native-chat): preserve large structured command results (#17707)
* fix(native-chat): preserve large structured command results

* chore: place native chat validation artifacts under docs

* chore: drop stale root package config

* fix(native-chat): enforce rebuilt lifecycle append slots

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 12:34:03 -07:00
Brennan Benson fd9125ea8c feat(native-chat): Codex structured native chat restructure (#16729)
* feat(native-chat): port structured Codex sessions from restructure-recovery

Rebuilds the desktop structured native-chat implementation from
brennanb2025/native-chat-restructure-recovery (tip 4e31c08db3) on top of
current main as a single commit, scoped to the local Codex path.

Ported:
- Structured agent-session core: durable record store + single-writer lease,
  canonical journal, agent-session wire host/attach/eviction/subscribers,
  `agentSession.*` RPC surface (registered via ALL_RPC_METHODS; host-side
  mobile allowlist included for wire compat), pty write gate, transcript
  additions, and the Codex app-server adapter/launch resolution.
- Renderer: NativeChatStructuredSession view/composer stack, structured
  launch path with the single-flight guard, local structured session tabs
  sync, activation gate + structured inventory (read-only
  `agentSession.handoffStatus` probe), agent-session tabs in the tab strip,
  AI-vault structured session activation, and the settings pane with the
  parent Experimental Chat UI toggle plus the nested "Use updated structured
  native chat" toggle. New sessions require both flags, agent codex, no
  prompt, and a local non-WSL, non-Windows-host execution host
  (structured-native-chat-availability).
- Fixes 72c013cea6 (verified Codex launch recovery), 8ddbaf5e3d (defer
  native terminal view switching affordances), and 4e31c08db3 (release the
  launch gate after a visibility retry) with their regression tests,
  including the third-launch-after-retry guard case.
- Cross-version agent-session wire test + CI lane, packaging entries
  (proper-lockfile, agent-tooling asar excludes), and the wire-compat doc
  section.

Deliberately not ported: mobile/ changes, the Claude structured runtime
(only the claude-transcript-branch-proof and claude-structured-owner-identity
leaf modules remain, backing the kept TUI-recovery arms), the terminal↔chat
adoption/handoff flow (`agentSession.adoptTerminal`/`requestHandoff`, the
handoff request engine, TUI adoption machinery, orca-runtime adoption
methods), renderer switching affordances and their dead leftovers, the
hook/subagent-status refactor cluster, and unrelated branch changes. The
crash-during-acquisition recovery path (restart handoff adjudication,
restore/reverse re-acquire, lease schema handoff keys) is kept because every
plain direct launch depends on it; a trimmed handoff coordinator exposes
only status/restore/close.

Branch edits that targeted files main has since split (ipc/pty.ts,
worktrees.ts, rpc/methods/terminal.ts, useIpcEvents, pty-connection,
store/slices/terminals.ts, runtime-types, web preload) were re-applied to
the split modules, preserving main's newer logic (Windows CIM fallback,
browser tab close rework, cold-restore resume flow, dispatcher threading).

Known seam: the mobile clipboard image-provenance CONSUMER gate ships
(agentSession.send refuses unproven mobile image refs with
agent_session_image_untrusted) but the producer hunk in
rpc/methods/clipboard.ts stays with the unported mobile cluster, so mobile
image sends into structured chat fail closed until that side ports.

* fix(native-chat): trust only authenticated local image uploads

* fix(build): preserve Windows process-tree patch application

* test(windows): include process creation time in addon fixture

* fix(build): run windows-process-tree node-gyp from the physical package dir

gyp expands the node-addon-api dependency by probing node, whose cwd
resolves to the package's physical directory in the store, so the emitted
target is a store-relative ../../../../node-addon-api@... hop. gyp then
resolves that hop against the rebuild cwd; from the node_modules
symlink/junction it escapes the store and configure fails with
"node_addon_api.gyp not found" (run 32999886072).

Rebuild from realpath(package dir) so both bases agree, matching how the
package manager itself runs native install scripts. The regression test
replays gyp's expansion+resolution against the planned cwd and fails
without the fix.

* fix(native-chat): keep chat tabs visible through terminal closes and empty-worktree launches

Two proven blockers in the native Codex tab contract:

closeTerminalTab pre-empted the canonical unified close. With one terminal
left it deactivated the worktree on a terminal/editor/browser-only check,
blanking a workspace that still held a renderable agent-session tab; with
two or more it pre-picked a successor from terminal entities only,
re-stamping the group active before closeUnifiedTab's MRU/neighbor repair
could land on the chat tab. Successor choice now defers to the unified
contract whenever the terminal has a unified row, and deactivation is
gated on the unified renderable count (matching leaveWorktreeIfEmpty),
with the legacy pre-pick kept only for terminals without a unified row.

A structured session created on an empty worktree was published into the
host's headless group while preserveLocalLayout froze the local layout,
leaving the tab in store but permanently off screen. A preserveLocalLayout
owner now always takes client-owned placement — repairing a rendered
leaf whose group record is missing, or materializing a rendered group on a
truly empty worktree — and applies the client-derived layout repair while
still rejecting host-authored layout.

Regression tests drive the real store through closeTerminalTab (git
worktree and folder workspace) and the real snapshot applier for the
empty-worktree adoption states; all fail without the fixes.

* fix(native-chat): close stale turns and retry rejected sends

* fix(native-chat): retire hosted rows on structured tab activation

* fix(native-chat): preserve rpc defaults across main merge

* chore: format remote wire compatibility guide

* test(native-chat): cover retry after unconfirmed send

* fix(native-chat): reload outbox on session switch

* docs(settings): disclose structured chat platform limits

* fix(native-chat): await Codex launch-home preparation

* fix(codex): align child-process allowlist with async trust bridge

* test(identity): update inventory for tab surface refactor

* fix(windows): preserve process-tree CRLF patch sources

* fix(native-chat): anchor an unmatched chat echo where it was sent (#16117)

* fix(native-chat): anchor an unmatched chat echo where it was sent

The reported symptom was old user messages replaying below every new turn, so the
conversation read as scrambled. The cause was not that the echo failed to match a
transcript row. Claude consumes a mid-turn send through a `queued_command`
attachment and writes no `type:"user"` record for it, so some echoes can never
match, and no amount of matching will change that. The cause was WHERE an
unmatched echo rendered: buildMobileNativeChatTransientData appended every pending
item after the entire transcript, so it re-read below each turn that landed
afterwards.

Render each echo directly after the transcript row it was sent against, using the
baseline the send already captures. An unmatched echo is then at worst a duplicate
in the right position rather than a scrambled one, and it stays visible. Echoes
sharing an anchor keep send order; a send with no baseline, or one whose anchor
folding dropped, still falls back to the tail.

Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an
echo can never match, then removing it, loses the user's own text for a message
the agent did receive, and it cannot fire in the common case anyway - measured
drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing
gap: the count pass has no baseline-tail guard, unlike the glue pass, while
`messages` is a 40-row window that head-trims, resets on reconnect and grows at
the front on loadEarlier, so a false landing there would license deleting a
DIFFERENT outstanding message.

That count-pass gap is real and left for a separate change; anchoring makes its
worst case a duplicate in place rather than a scrambled conversation.

* fix(native-chat): preserve folded echo anchors

* fix(native-chat): preserve forward-folded echo anchors

* fix(native-chat): keep leading folded echoes in place

* fix(workspace-cleanup): show git status for every row (#16690)

* fix(native-chat): refuse structured chat on every Windows execution path

canUseStructuredNativeChat only refused win32 when a project runtime
resolved, so folder-workspace keys (and other keys with no project
runtime) failed open into structured chat on Windows. Fail closed on
win32 unconditionally after the host check, matching the settings copy:
local macOS/Linux only; Windows/WSL/SSH stay on terminal chat.

* fix(native-chat): restore runtime refusals behind the win32 gate

506d375de3 replaced the project-runtime checks with a bare platform test,
so a WSL or repair-required runtime resolution would no longer refuse
structured chat off-win32. Keep the unconditional win32 refusal and
re-run the runtime resolution after it, so the gate does not depend on
the resolver's own platform guard. Tests inject WSL and repair-required
resolutions on darwin/linux and fail against the regressed gate.

* fix structured session journal durability

* fix structured tab active pointer after restart

* fix(native-chat): await optional lease renewal callbacks

* refactor(skills): extract install error messages

* fix(agent-session): harden recovery ownership

* fix(native-chat): retain panes across tab activation

* fix(native-chat): address round-one review findings

* test(native-chat): align integration coverage after main merge

* fix(native-chat): harden round-two reliability

* fix(native-chat): harden round-three reliability

* fix(native-chat): close round-four recovery gaps

* fix(native-chat): separate bounded journal key forms

* fix(native-chat): reset outbox error in render on session switch

The switch effect adjusted error state after the sessionId prop changed,
tripping react-doctor's no-adjust-state-on-prop-change on the changed-code
gate and flashing the old session's banner for a frame. Reset it with the
render-time previous-value guard instead.

* fix(native-chat): invalidate stale outbox settlements

* test(native-chat): restore settled-error session-switch regression

a6e2379bd1 replaced this test with the in-flight settlement race test,
leaving the render-time error reset unpinned: deleting the reset block
still passed the whole native-chat suite. Keep both scenarios pinned;
they are distinct (settled error clears on switch vs stale settlement
invalidated in the commit-to-passive window).

* test(wire): make release checkouts race safe

* test(wire): pin cross-process checkout single-flight and importer specifier contract

* test(wire): harden release checkout lifecycle

* fix(build): drop CR-byte residue from windows-process-tree patch

The two trailing CR bytes on the patch's deletion lines are a proven
no-op: pnpm hashes patches CRLF-normalized (both forms hash to the
lockfile's 946ffb2b) and materializes this package without applying the
patch in either form, so the load-bearing build edits come solely from
applyWindowsProcessTreeBuildFixes() (#16947), which handles both source
EOL forms. Restore byte-identity with main and repin the contract test
to the post-#16947 reality: LF-only patch bytes plus lockfile hash sync.

* fix(native-chat): skip empty startup recovery
2026-08-28 16:45:58 -07:00