mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
ccb63afc06ebc7f232c1173bf5db5b2cf420ebdd
12332
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ccb63afc06 |
fix(native-chat): a Stop still reads as yours after Orca restarts, because the turn's end reads the Stop's event (#24311)
* test(native-chat): a Stop over a card sent now into the running turn keeps it paused
Red on main: Codex's turn end withdraws the steered hand-off and the queue
sends the card again as a new host turn, with no pause recorded.
* fix(native-chat): a Stop's queue pause holds a card whose hand-off is still unanswered
A card sent now into the running turn was still pending when Stop judged the
pause, so nothing was recorded; the interrupt then withdrew the hand-off, the
card went back to waiting unpaused, and the queue sent it again as a new turn.
The pause now counts a hand-off that may still return to waiting, judged with
the appended row applied, so a withdrawal lands under the pause and an
acceptance retires it in that same write. Codex and Claude both hit it.
* test(native-chat): the Claude re-send case fails on its diff, inside the test's budget
* test(native-chat): a pause held by an unanswered hand-off ends on every path that ends it
The provider's answer, the provider dying, the chat closing, a restart, a
withdrawal still owed at open, and a /clear (refused until the hand-off ends,
then carrying every waiting card paused 'cleared'); each ends with the queue
sending again.
* fix(native-chat): narrow the pause's settled hand-off, and assert the queued receipt's card
* refactor(native-chat): derive the queue's pause from Stop and Resume journal rows
Stop now appends one journal row where it takes effect, before the interrupt,
whatever the queue holds; Resume appends its own. The pause is a pure function
of the fold: the latest Stop with no later Resume and no later accepted turn a
person asked for. A /clear's carried cards name their source, which is the
replacement's 'cleared' pause. Host-origin turns never lift either.
One predicate decides which cards a pause holds; by default every waiting card
without a hold of its own, including one queued after the Stop. The drain's
consume re-judges it inside its own transaction.
The rows are tombstones of an id no item takes, carrying the mark: a released
host reads an unknown row kind as corruption and truncates the journal there.
Deletes the stored pause (recordPause, the retire hook on every appended row,
the settle-before-record step, mayReturnToWaiting and its row overlay) and the
tests that only proved it retires. The queued_message_pauses table stays in the
schema, unread and unwritten, for downgrade safety.
* fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones
A Stop's pause now holds only the cards queued before its row, plus a steer it
withdrew, which returns to its own place. Each card records the journal
position it was queued at, and the one hold rule compares that with the Stop
row. A card queued after the Stop is a new instruction: it sends as usual, but
the drain still stops at the first held card, so it never overtakes them.
/clear's pause holds the cards it carried. Holding every card again is a
one-line switch in that rule.
* fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction
The drain's pick and its consume now read one function, nextSendableQueuedCard,
so a Stop row that lands between them holds a newer card behind an older held
one exactly as the pick would. Notes why Stop and Resume ride a tombstone row.
* fix(native-chat): stop creating the unused queue pause table
The queue's pause is derived from journal rows, so nothing reads or writes
queued_message_pauses. It was still created on every open "for downgrade
safety", but an older build creates it itself when it opens the database, so
the table only sat empty in every new database. The tests now pin that no
pause table exists.
* fix(native-chat): a Stop's pause never hides the restart pause
A Stop holds only the cards queued before it. The pause derivation still
returned the Stop alone whenever it was in force, so the restart pause was
never considered: a card queued after the Stop, written by a host process
that has since exited, sent by itself after Orca restarted, with no pause
header and no Resume. A /clear pause that held nothing could hide it the
same way.
Every pause in force is now derived. A card is held if any of them holds
it, and it names the first that does. The drain's pick, the consume
transaction's re-check and the published header all read that one rule;
the header names the pause holding the first card Resume would send.
* test(native-chat): pin the Stop's no-resend, lift and held-card rules
- The Claude and Codex Stop-withdraws-a-steer tests checked "not sent
again" at one instant, before a queue ignoring the pause re-sends. They
now wait for the stopped turn to end and re-check after a quiet window.
- The deleted-card test read a card queued after the Stop, which sends
whether or not a person's turn lifts it; it now reads the Stop's pause
before and after that turn.
- Unit cases pin that a Stop holds a card with no recorded position and one
queued before a rewind.
* refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller
The Stop row that paused the queue becomes the general Stop event
{ reason, turnId?, at, caller? }, whose reason is the host's existing stop
cause. It still rides a tombstone of a host-only id (a released host deletes
the journal from the first unknown row kind), and Resume keeps its own marker
on its own id. Only a person's Stop (reason user-stop) pauses the queue.
* test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event
* test(native-chat): pin that Stop and Resume rows never reach apps or count as history
* test(native-chat): only a person's Stop event pauses the queue
* test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop
Through the real host: the event names the turn and who asked and is in the
journal when the interrupt reaches the agent; at an agent still starting it is
there before the start is ended and holds a card queued before it; an idle Stop
writes one only when it withdrew a send; and the queue's claim re-judges a
pause that landed after its pick.
* test(native-chat): a card held at a starting agent is checked before the Stop's timing
Also says precisely what the claim's in-transaction pause check defends
against: the Stop and the drain share one serialized lane.
* test(native-chat): a released build keeps and folds a journal holding Stop events
Replays this build's rows from the released build's own journal database: every
row is kept, the history after the Stop still folds, and an older client is sent
only removed ids no item uses.
* style(native-chat): format the Stop event changes
* test(native-chat): type the released build's exports through one checked helper
* fix(native-chat): the Stop/Resume row guard narrows to those tombstones only
* test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade
The Stop-event downgrade test ran in no CI lane: unit shards exclude the
cross-version folder, and the cross-version lane runs a fixed file list that
did not name it. It is now on that list.
Its only case replayed the rows into a release's own fresh database, because
that release cannot open the current host database. A second case opens the
journal this build wrote with a main build that shares the database: it opens
writable, keeps every row, appends, and this build then reopens it with the
person's Stop still pausing the queue.
* fix(native-chat): a Stop that stops nothing new writes no Stop event
A Stop reaching a running agent wrote a Stop event on every press. Two
presses before the first interrupt landed wrote two events, so a card
queued between them counted as before the latest Stop and was held,
though a card queued after a Stop should send normally. A Stop naming a
turn that had already ended, as a phone sends late, also wrote an event
for a turn it never stopped.
It now writes one only when it withdrew a queued send, or stops something
no event records yet: not a turn the journal no longer runs, and not the
live turn a Stop still in force already names, unless a card was handed
over into it since, which this Stop's interrupt sends back and must hold.
The interrupt and the "already finished" note are unchanged. A Stop at a
starting agent still always writes.
* test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop
* chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled
* fix(native-chat): any later Stop event ends a person's Stop pause
A person's Stop paused the queue until their next accepted turn or Resume,
and a later Stop of another reason (the host stopping the agent, an
eviction, a close) was ignored. Now the pause is the latest Stop event's:
a later Stop of any reason ends a person's pause, and only a person's Stop
pauses. The fold keeps the latest Stop event whatever its reason.
An eviction of a resting chat writes no Stop event (a Stop that stops
nothing writes nothing), so it cannot release held cards; a test pins that
no event means no lift.
* fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed
A Stop pressed before the agent's turn shows in the journal (before
Claude's echo, or before Codex opens the turn) records no turn. A second
press once the turn showed compared that missing turn with the live one,
wrote a second Stop event, and held a card queued between the presses.
A repeat is now judged by what was sent since the Stop in force: with
nothing sent after it (a refused send aside), a Stop that named no turn,
or named the live one, is repeated and writes nothing. Anything sent since
and not refused, including a send whose fate is unknown, makes the new
press write, since its interrupt may send that card back to waiting.
Tests: the two-press case across the turn showing; a steer between the
presses settled unknown; and a Stop naming a turn that ended while the next
card is sent but shows no turn yet, which writes and holds that card. The
fold test that claimed an eviction path is renamed.
* fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes
A Stop or Resume row's value is read from disk with no shape check, and
the pause fold stored whatever it found. A stored `stopEvent: null` would
then throw on every pause check for that chat: the queue's pick, its
send, and every queue update to clients. No build writes such a row, so
this is hardening.
The fold now reads a Stop only when it is an object with a string reason
and a finite time, and a Resume only when it is `true`. Anything else is
ignored: it pauses nothing and ends nothing. The row is still not treated
as malformed, which could cut the history short.
* fix(native-chat): a Stop still reads as yours after Orca restarts before the turn ends
Every stop that ends work now writes the Stop's event before it ends the child: a
person's close of the chat, an eviction (worktree teardown, orchestration stop, tab
cleanup) and the idle sweep's stop of a start that never landed. A stop that ends
nothing writes nothing, and quit writes none: its resume marker records why.
The turn-end write reads the latest Stop event where every turn row is built, so the
adapter's settle, the host's fallback and the relaunch's settle all agree: a turn a
person's Stop or close named, ending with no verdict of its own after that Stop, ends
as their cancellation. A relaunch's probe-bounded end is no earlier than a Stop that
found the turn running. When the provider refuses the interrupt and the turn runs on,
a refusal row answers the Stop, so a later crash still reads Failed; pressing Stop
again after a refusal is a new Stop.
* refactor(native-chat): a stop no longer carries its cause; the turn's end reads the Stop event
The cause of a stop was threaded in memory from each entry through the host's stop
step, the adapter router and each adapter's close onto the `ended` it settled with,
and Claude kept a per-turn copy of a Stop it sent. All of that is gone: adapters
settle a turn they cut as interrupted with no verdict, the host's fallback does the
same, and the one rule where a turn row is built (`turnEndAfterStop`) reads the
journal's latest Stop event to say whether it was a person's.
- `closeSession` / `disposeSession` take no cause; `ended` has no `stopCause`.
- Claude reads an error result after a person's Stop as their cancellation from the
journal's Stop event (through the event sink), not from a per-turn slot, and a
refused interrupt is the host's refusal row, not `withdrawTurnStop`.
- An owed wind-down keeps no cause: its retry's fallback reads the Stop event.
- The mutation context's Stop passes no cause: its step already wrote the event, and
the delivery loop's child-end reason is read back from it.
- A Stop pressed before its turn showed applies to the turn that opens under it,
unless a send a person made since was accepted.
* test(native-chat): a turn a later send opened is no Stop's that named no turn
* test(native-chat): the restart test's death proof carries its detail
* refactor(native-chat): a refused Stop leaves no record; a Stop only ever ends the turn it names
The stop-refused mark is gone: its tombstone kind, its fold, the clock-keyed match that tied it to
a Stop, and the exception that let a second press after a refusal write a new Stop. A Stop that
stops nothing writes nothing. A Codex refusal names a turn that is no longer its active one, and
the Stop names that turn, so the turn running instead never reads as the person's by its id alone.
* fix(native-chat): a Stop pressed before any turn showed stops only the turn opened next
A Stop that named no turn read as the person's cancellation for every later turn that opened
after it, until a send a person made was accepted. The queue's drain, orchestration mail and a
restart continuation send as the host, so a turn they opened long after, cut by a crash, read
"Interrupted" as if the person had stopped it. The Stop now applies only to the first turn
opened after it.
* fix(native-chat): an older Claude's error end after a Stop pressed before its echo reads Interrupted
Claude CLIs before 2.1.91 end an interrupted turn with an error result that names no reason. The
translator judged whether a person's Stop explained it by its own copy of the Stop rule, which
ignored a Stop that named no turn, so a Stop pressed before Claude echoed the send read "Failed".
The translator now writes such an end as interrupted with no verdict and no error row whenever a
person's Stop may name the turn, and the journal's one rule decides as it writes the end.
* fix(native-chat): a person's Stop and /clear each name why they end the agent
The host's mutation path ended the agent with one "recorded" ending for every caller, which read
back the reason of whatever Stop event the journal held last, however old. /clear writes no Stop
event, so its end took an unrelated earlier reason. Each caller now names its own: the chat's Stop
`user-stop`, whose event its own step wrote, and /clear `user-close`, the user replacing this chat.
* fix(native-chat): a host stop judges whether it ends work after the provider's rows land
A close, eviction or host stop decided whether it ended a running turn from the journal as it
stood, while the provider's own rows (the turn its echo opened) could still be in the session's
event sink. A close landing in that gap wrote no Stop event, so the turn it cut read as news. It
now reads after the sink drains, as a person's Stop does, through the same check; a drain that
fails or takes over a second reads working.
* fix(native-chat): a Claude Stop naming a turn that just ended still marks the follow-up it cuts
A phone names the turn it last saw. When that turn had ended and a follow-up was still unechoed,
Claude's Stop interrupted the follow-up and ended the child, but the Stop's event named the ended
turn, so the follow-up's turn the child's end cut read "Failed" under "Cancellation requested.".
A Stop that ends the provider's session ends whatever is in flight, so its event now names the
live turn or none, and a Stop that names none binds the turn opened next. Codex keeps naming only
the turn the Stop names.
The Claude Stop turn-end tests move to their own file, since the session-ending Stop suite is at
its line budget.
* fix(native-chat): the idle sweep reads working by the same rule as a stop's event
The sweep judged a chat resting while a send whose reply was lost was still unanswered, but the
stop's event writer counts that send as work. So the sweep evicted it and wrote an evict event,
which ends a person's Stop pause and let the cards behind it drain on their own. The sweep's owed
work now reads the main agent working the way every session list and the event writer do.
* test(native-chat): an aborted eviction's injected drain failure lands on the eviction's own drain
A host stop now drains the session's sink once to judge whether it ends work, so the tests that
fail the eviction's drain-published step skip that first drain.
* fix(native-chat): the idle sweep's rest writes no Stop event; it evicts a send that never echoes
The previous commit made the sweep count an unanswered send as owed work, which pins a chat whose
admitted send Codex never echoes forever, and the sweep exists to retire exactly that. That rule
returns. The sweep stops only an agent it judged resting, so its eviction now writes no Stop
event, whatever send it retires: a person's Stop pause holds through it.
* fix(native-chat): stopping a start that carries no send writes no Stop event
A host stop, eviction or close of a starting child wrote a Stop event whatever the start carried.
A start with a send already reads working, so the clause only mattered for a start with none,
which ends no turn and no send: its event only lifted a person's Stop pause and bumped the idle
clock, which is why the idle sweep had been changed to close the conversation in the same pass.
The clause goes and the sweep is #24072's again. The child's end still reads host-stop, as before.
* test(native-chat): a Stop's pause across a restart is tested with a restart that writes no event
The rig's restart closes the chat with an eviction, which now writes a Stop event when work runs
and so ends a person's Stop pause. "A Stop never hides a restart's pause" then passed with no Stop
pause left to hide anything. Those tests, and the pause-lift test whose dropped assertion returns,
restart as a process that dies with no close, which like a quit writes no Stop event, and assert
that both the Stop's and the restart's pauses are in force first.
* fix(native-chat): a host stop of a turn a person's Stop is still ending keeps that Stop's reason
An eviction or host stop that landed while a person's Stop or close was already ending the same
turn wrote a newer Stop event, and the turn's end reads only the latest, so the person's Stop of
that turn read as news. A host reason now writes nothing while a person's Stop still decides what
runs: the live turn it names or bound, or, with none, the turn a send opens next. The person's
own close still writes. The E2 tests now open and end the stopped send's own turn, as Codex does,
so the mail turn after it is not the turnless Stop's.
* fix(native-chat): an older Claude's error on a later turn keeps its error text after a Stop
The translator left an error result that names no reason to the journal's Stop rule whenever a
person's Stop named the turn or none, but the rule binds a Stop naming no turn only to the turn
opened next. So a real error on a later turn read "Failed" with its error text dropped. The
translator now asks the journal's rule itself (`personStopDecidesTurn`, the one core
`turnEndAfterStop` and a host stop's in-force check share), so the two cannot disagree.
* fix(native-chat): a Stop of a start that never landed binds no later turn, whatever sent it
A person's Stop pressed while the agent starts names no turn, and the send it stopped is
cancelled before it opens one. The Stop then bound the next turn anything opened (orchestration
mail, a restart continuation, the queue's drain, all of which send as the host), so a host
eviction of that turn wrote nothing and its crash or close read as the person's cancellation. A
Stop that named no turn now binds only a turn no send journaled after it opened: any send since,
of any origin and not refused, opens its own. The E2 test's mail send is accepted as Codex
accepts it, instead of opening the stopped send's own turn first.
* test(native-chat): a rewind's restated turnless Stop binds no turn opened after the rewind
A Codex rewind restates a person's Stop still in force after the turns it keeps, at a new
sequence, so by sequence alone it would bind the next turn opened after the rewind. A send
journaled after the restated row voids that binding (the previous commit), which this pins.
* fix(native-chat): a relaunch settles a person's stopped turn with no "stopped while in progress" row
After a restart, a turn a person's Stop ended reads "Interrupted after N" with the muted mark, but
the relaunch still added the error row saying the provider stopped mid-response, which a live Stop
never writes. The settle now skips that row when every turn it interrupts is the person's Stop's
by the journal's one rule; a crash nobody stopped keeps it.
* test(native-chat): the unexpected-exit settle's journal fake answers whether a person's Stop decides a turn
* fix(native-chat): a host stop whose sink drain fails reads the journal as it stands
A host stop drains the session's sink before judging whether it ends work, and a failed or slow
drain read as working. So an eviction of an agent at rest wrote a Stop event that ended nothing,
which lifts a person's Stop pause, and a close wrote a person's event naming no turn. The drain is
now best effort: the stop goes ahead either way and only its record is at stake, so a failed or
slow drain leaves the journal's read as it stands. A person's Stop keeps its own rule.
* fix(native-chat): a Stop that named no turn applies only to a turn a send it stopped opened
A person's Stop pressed before any turn showed names no turn. It bound the first turn opened
after it, then (
|
||
|
|
1553bc3b80 |
fix(terminal): reattach a background terminal to its own tab instead of opening a duplicate (#24458)
* fix(terminal): reattach a background terminal to its own tab instead of opening a duplicate When a workspace with already-running terminals is opened and the renderer has lost a tab's link to its terminal, the activation gate asks the host who owns each unlinked terminal. The host's graph only carries mounted or provably live panes, so an unmounted tab whose link was lost reads as "no surface", and the gate opened a brand-new tab on the running terminal: the same terminal then showed in two tabs once the original tab mounted and reattached. The host now names the pane it last recorded for an orphaned terminal (`recordedPaneKey`, optional). The renderer, which owns its tabs, rebinds the terminal there when it still holds that pane free, and opens a tab only when the pane is gone or holds another terminal. * test(terminal): pin that an unowned PTY never rebinds to a vanished leaf or another worktree's tab The rebind to the host-recorded pane relies on two existing guards in the exact-surface binder: the recorded leaf must still be in the tab's layout, and the tab must belong to this worktree. Neither was pinned on the unowned path. Both new cases mint a fresh tab and leave the recorded tab untouched. |
||
|
|
f69052e113 | Reuse qualified Windows server builds and dependency verification records (#24448) | ||
|
|
38c2d1dcb9 |
feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)
* feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) Builds managed-server maintenance on T6-2's deploy, rollback and recovery and T6-4's journaled decommission. Each step reads a terminal census through the server's tunnel; orcad answers it through a new capability-gated orcad.terminalCensus RPC, and an older host or lost answer is unverifiable. Update defers over live or uncounted terminals and status reports the last deferral. A stop unlinks the server (deployment record, tunnel, SSH claim) only after a proven exit. Inert until the T6-6 settings UI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(rpc): load the orcad terminal census lazily so the dispatcher does not pull in the xterm window polyfill Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
eefc49f7e3 |
feat(terminal): warn when a typed Codex joins Codex's shared server (STA-9051) (#24217)
* feat(terminal): warn when a typed Codex joins Codex's shared server Orca adds --no-daemon to the Codex it launches and to a codex typed in shells whose wrapper it controls, but a codex typed another way (fish, cmd.exe, a path-named binary) still joins Codex's shared server, which mixes up agent status across tabs. When a local pane's Codex is on that server, show a banner at the top of the pane with the command that turns auto-start off, a Copy button, "Don't show again" (a new setting next to the Codex server setting) and a per-pane dismiss. The banner takes layout space; the terminal refits below it. Main answers pty:isCodexOnSharedServer from the pane's outermost Codex command line (flags and subcommands that keep Codex embedded rule it out), the CODEX_HOME the pane launched with, and whether that home's server is live: a socket connect on macOS/Linux, the server's pid record plus creation time on Windows. The renderer asks only while the pane already shows Codex, on a short bounded ladder. Refs STA-9051 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: restore the Claude WSL trust-file fix (#23973) dropped by the banner commit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(terminal): redesign the Codex shared-server banner and fix dialog Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(terminal): let the Codex shared-server fix run its commands The fix dialog now runs each step with the shared server's own Codex on the pane's CODEX_HOME, verifies the result (feature read back, server probed), and falls back to a copyable command on failure. Stopping asks first. Also: an apostrophe in a prompt no longer hides an opt-out flag, restored panes fall back to the saved pty id, and the IPC guards have a table test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(terminal): give each fix step its own card and label the command it runs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(terminal): simplify the Codex shared-server banner after review - Read a subcommand only from Codex's first positional, so prompt words like "a", "update" or "review" no longer hide the banner. - Probe the server fresh on every ask; drop the probe cache. - Make the pty preload methods required and stub them on the web client, replacing the optional-method and paired-client checks. - Render the banner from the existing Codex pane portal loop. - Treat a non-zero or timed-out Codex command as failed; skip the read-back when the disable write failed. - Reserve the banner's space with a CSS :has() selector instead of a data attribute. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(terminal): make the Codex shared-server fix persist on Orca's mirror home - Step 1 now writes daemon_auto_start = false to the user's own Codex home when the pane runs on Orca's shared mirror home (as Windows panes do), then to the mirror home too; the mirror is rebuilt from the user's home on every launch, so a mirror-only write was lost. - The server probe is three-state (live / absent / unknown); stop reports success only once the server is proven gone. - The banner retires the one-time "runs Codex without its shared server" toast it contradicts. - A command line with no Codex program never counts as joining the server. - The fallback local PTY provider reports each pane's root pid. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(codex): keep Turn off in Orca's Codex home when ~/.codex has no config A pane on Orca's mirror home wrote the setting to ~/.codex first. With no ~/.codex the spawn failed on its cwd and Codex rejects a missing CODEX_HOME; and creating a config holding only this setting would make the next mirror replace every setting made in Orca's Codex. The mirror skips a missing or blank ~/.codex/config.toml, so write only the mirror home then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(codex): promote [features].daemon_auto_start from Orca's Codex home Promotion now carries one [features] key alongside the [tui] keys, so a shared-server Turn off written in Orca's mirror home reaches ~/.codex/config.toml instead of being reverted by the next mirror. Table keys share one <table>.<key> scan for read, removal and upsert. Orca's own daemon socket override is never read as a user value, and a blank source config is seeded from the runtime like a missing one. * refactor(codex): run Turn off once, in the pane's own Codex home Settings promotion now carries the setting to ~/.codex, so the separate settings-home resolution and the two-home loop are gone. * fix(terminal): offer Stop server only after sharing is turned off Stopping while sharing is still on closes every sharing session, and the next Codex starts a new shared server. * fix(terminal): skip legacy mirror panes off Windows and quoted dotted keys A retained shared-home pane on macOS/Linux points at a mirror that is no longer promoted, so Turn off there would be reverted; name no home for it. A quoted top-level key such as "tui.theme" is one key, not [tui].theme. * fix(terminal): drop the Turn off note that promised the setting reaches Codex outside Orca Orca's tabs are what this fix is for; carrying the setting to ~/.codex is best-effort. * fix(terminal): keep the Turn off note that the setting also applies outside Orca It holds for nearly everyone; the rare Windows upgrade gaps don't justify hiding it. * fix(codex): promote Turn off to ~/.codex under an older Orca's baseline A pane on Orca's Windows mirror home writes daemon_auto_start = false into the mirror. A promotion baseline from an Orca that predates this key has no entry for it, so the next mirror pass kept the write as a conflict and then recorded it, and ~/.codex never got the setting. Turn off on a mirror-home pane now runs the same mirror pass a terminal launch runs before and after the write: the first records the key in the baseline, the second promotes the write to ~/.codex. The passes are synchronous, so they cannot interleave with a launch's pass. A failed pass is logged and does not fail Turn off, since the write still fixes Orca's tabs. Real-home panes are unchanged. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
8b76683b40 |
feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)
* feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) Adds deploy + pair + status for a managed orcad environment on an empty SSH host, the loopback tunnel it is reached through (rebuilt on reconnect and after host resume), SSH provisioning of a new host, and SSH access for an already paired server. The deployment link lives in the environment sidecar so a downgraded build cannot strip it. Inert until the T6-6 settings UI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ssh): refuse a managed claim while saved state still references the host Until the T6-8 census exists, a target is claimable only when no workspace session, automation, worktree metadata or saved PTY lease (any status) points at it. An unreadable store refuses as unverifiable. Refusals name what blocked them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ssh): keep electron out of the resume path; report a decommission journal in status The caller now passes the profile path for managed-tunnel recovery after host resume, so ssh-host-sleep-reconnect no longer reads electron's app. Status maps T6-4's decommission transaction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
b804c13934 |
test(runtime): add a readiness census that pins every tui-idle verdict (#24336)
* test(runtime): add a readiness census pinning every tui-idle verdict Replays every recorded agent PTY transcript frame by frame through a real runtime pane (agent-known and agent-unknown, clocked and clockless) and a synthetic evidence matrix for all 43 TuiAgents, and compares each verdict and tui-idle wait outcome to committed run-length-encoded baselines. Refs STA-9098 * test(runtime): pin the census quiet probes to literal windows A census that read TUI_IDLE_QUIESCENCE_MS would move with it; fixed 2999/3000 ms reads and a fixed 2000 ms poll step make a changed window show as changed verdicts. Refs STA-9098 * test(runtime): say which census probe writes runtime state Refs STA-9098 * test(runtime): observe the census through settled panes and caller-visible waits - Read each verdict through the runtime's own settle seam (evaluateTuiIdleForLeaf) instead of re-wiring evaluateTuiIdle/leafTuiIdleEvidence/buildTerminalWaitText, so the census is coupled to one runtime method, not to the module STA-9098 rewrites. - Let the runtime finish each chunk (one macrotask turn) before reading. The old read raced work chained on the paint, so 14 frames pinned a microtask-ordering artefact. - Record when a wait settles (@start vs @poll), not just its outcome. - Exit each pane's PTY after reading it so its emulator is freed. - Replace the hand-grouped families, literal fixture list and per-pane split flag with a directory-scanned catalog, one baseline per replayed pane, and size-balanced shards. - Run the synthetic matrix in one file; it takes about 2 s. * test(runtime): cross dialog-versus-ready-screen order with every title in the census matrix Blocked detection is position-ordered (design doc 11.5): the later of a blocker and a ready anchor wins. The matrix now paints a workspace-trust dialog after, and before, each agent's ready screen under every title, so a rule engine that loses that ordering fails per agent. * test(runtime): read the census baseline field without Reflect.get The anti-slop lint rejects Reflect.get on parsed input. |
||
|
|
d3f8c5063b |
fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)
GC pins every slot an in-flight activation journal names and skips the pass entirely when a journal is unreadable or a fence is held without one. orcad's self-test now reports the coverage the daemon says its probe achieved, and remote readiness probes accept a slot only on pty-spawn coverage, or handshake on win32; builds without the field keep the identity gate. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
43d9b43d3f |
feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)
Clients stop an orcad that advertises health.stopRequests through its slot-local request file and keep SIGTERM for older builds. Decommission runs through the activation journal and fence: it refuses while the terminal census is live or uncounted, stops the instance with an instance-bound managed request, cancels a stop orcad never acted on, and deactivates the record only on proven exit. orcad gains --cancel-managed-stop and an exclusive per-transaction decision file so a cancel can never race a dispatched stop. POSIX-only and inert: no production caller. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
fd5804dd6a |
fix(native-chat): after a Stop whose exit can't be confirmed, the next message retries the stop instead of failing (#24333)
* feat(native-chat): a status for a message waiting on an exit Orca could not verify Adds the `previousExitUnverifiable` failure fact, a status row only, never a reason a message was not sent: Orca couldn't confirm the agent's previous process ended, and the message will send once it has. Copy in every catalog. * fix(native-chat): a message after a Stop whose exit was unproven retries the stop, then waits When a Claude Stop could not prove its child gone, the child stayed in place with a connection that refuses writes, so the next message failed with "Orca couldn't hand this message to the agent" and every later one did too, until the idle sweep retried the stop after 30 minutes of quiet. The delivery loop now retries an owed stop before it starts or writes to a child, once per new message. Still unproven, the message stays queued under one warning row saying why, and the idle sweep's next tick retries the stop, child or not, and hands the message over once the exit is proven. * fix(native-chat): every operation that reaches the agent finishes an owed stop first Option changes, card answers, background-task stops, goal changes and rewinds went to a child a Stop could not prove gone, whose connection takes no input. The retry now lives in one place, `finishOwedStructuredAgentSessionStop`: `ensureStructuredAgentSessionAgent` calls it before reporting or starting a child, and operations on the running child prepare through it. Still unproven, it refuses with `previousExitUnverifiable` and the exit `unverifiable`; the delivery loop turns that refusal into the visible wait, so a send still waits under its one row instead of being refused. * test(native-chat): type the option-change envelope in the unproven-stop test * fix(native-chat): the stop that proves the exit hands over the message that waited on it - The stop itself wakes delivery where its owed wind-down clears, so a message held on an unproven exit goes out whichever retry lands: an option change, a background-task stop, the sweep or the next message. Only the sweep woke it before, so an option change that proved the exit left the message queued with nothing to send it. - The owed stop keeps where it was asked for, and the child's end is ordered there. A message accepted while retries ran is no longer rejected as "chat closed" (a tab close) or failed as a host stop when the retry that proves the exit lands after it. - A wind-down owed by an earlier child never stops a different live child in front of it. * docs(native-chat): say who retries a wind-down a Stop leaves owed * fix(native-chat): a waiting message retries the unproven stop once, and its note stays true - A waiting message holds against the newest pass that failed to prove the exit, kept on the owed-stop record (`failedAt`), not against the note's position. The note is written once per process, so after a second message every later journal commit re-ran a retry of up to 10 s. - The note no longer promises this message will send: "Messages wait to be sent until Orca confirms it has ended." stays true after a Stop withdraws the message, a close, or a restart rejects it. Every catalog follows; es and zh lose the mismatched informal possessive, and the French reads naturally. - Tests: commits after a second waiting message retry nothing; with follow-up queueing on (the default) a follow-up becomes a draft, and Steer retries once and waits under the same note. * fix(native-chat): only a retry continues an owed stop; a new close is a new ask A second tab close of a chat whose exit stayed unproven kept the first close's position, so a message sent between the two closes was delivered once a retry proved the exit, even though the second close closed it (its own rejection of what was queued had failed). Continuation is now explicit: only the retry of an owed stop passes `retry`; every other stop stamps a new position. Tests: a second close whose rejection fails still closes the message it closed; the default- queueing test waits for a draft's commit to settle before asserting it retried nothing, and bounds Steer's retries; the one-retry-per-message test has the budget to show each commit's retry. * fix(native-chat): a held message always says why it waits When another operation's retry of the unproven stop failed between a message's accept and its delivery step, the step held the message (it had already waited through a failed retry) and stopped before writing the note, so the message sat Working with no reason shown. The hold now makes sure of the note too; it is written once per unproven process, so its own commit still retries nothing. * fix(native-chat): an option change waits only on an unproven exit, not on bookkeeping When a stop proved the old process gone but a later step of its wind-down kept failing, an option change retried that bookkeeping, and refused the change when it failed again. On main the change was kept at rest. Operations that start no child (option change, card answer, background-task stop) now hold back only while the exit itself is unproven, its child still on record; with the exit proven, the bookkeeping retry is reported and the operation goes on as with nothing owed. Sends still wait: a start needs the released lease that bookkeeping gives back. * test(native-chat): type the unproven-stop test's envelope fields by the fingerprint's own shape * test(native-chat): a message after a Codex Stop whose exit was unproven retries that stop, then goes to a fresh Codex |
||
|
|
976dc00337 |
fix(native-chat): Stop's pause is worked out from the chat's history, so a steered message is never re-sent (#24072)
* test(native-chat): a Stop over a card sent now into the running turn keeps it paused
Red on main: Codex's turn end withdraws the steered hand-off and the queue
sends the card again as a new host turn, with no pause recorded.
* fix(native-chat): a Stop's queue pause holds a card whose hand-off is still unanswered
A card sent now into the running turn was still pending when Stop judged the
pause, so nothing was recorded; the interrupt then withdrew the hand-off, the
card went back to waiting unpaused, and the queue sent it again as a new turn.
The pause now counts a hand-off that may still return to waiting, judged with
the appended row applied, so a withdrawal lands under the pause and an
acceptance retires it in that same write. Codex and Claude both hit it.
* test(native-chat): the Claude re-send case fails on its diff, inside the test's budget
* test(native-chat): a pause held by an unanswered hand-off ends on every path that ends it
The provider's answer, the provider dying, the chat closing, a restart, a
withdrawal still owed at open, and a /clear (refused until the hand-off ends,
then carrying every waiting card paused 'cleared'); each ends with the queue
sending again.
* fix(native-chat): narrow the pause's settled hand-off, and assert the queued receipt's card
* refactor(native-chat): derive the queue's pause from Stop and Resume journal rows
Stop now appends one journal row where it takes effect, before the interrupt,
whatever the queue holds; Resume appends its own. The pause is a pure function
of the fold: the latest Stop with no later Resume and no later accepted turn a
person asked for. A /clear's carried cards name their source, which is the
replacement's 'cleared' pause. Host-origin turns never lift either.
One predicate decides which cards a pause holds; by default every waiting card
without a hold of its own, including one queued after the Stop. The drain's
consume re-judges it inside its own transaction.
The rows are tombstones of an id no item takes, carrying the mark: a released
host reads an unknown row kind as corruption and truncates the journal there.
Deletes the stored pause (recordPause, the retire hook on every appended row,
the settle-before-record step, mayReturnToWaiting and its row overlay) and the
tests that only proved it retires. The queued_message_pauses table stays in the
schema, unread and unwritten, for downgrade safety.
* fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones
A Stop's pause now holds only the cards queued before its row, plus a steer it
withdrew, which returns to its own place. Each card records the journal
position it was queued at, and the one hold rule compares that with the Stop
row. A card queued after the Stop is a new instruction: it sends as usual, but
the drain still stops at the first held card, so it never overtakes them.
/clear's pause holds the cards it carried. Holding every card again is a
one-line switch in that rule.
* fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction
The drain's pick and its consume now read one function, nextSendableQueuedCard,
so a Stop row that lands between them holds a newer card behind an older held
one exactly as the pick would. Notes why Stop and Resume ride a tombstone row.
* fix(native-chat): stop creating the unused queue pause table
The queue's pause is derived from journal rows, so nothing reads or writes
queued_message_pauses. It was still created on every open "for downgrade
safety", but an older build creates it itself when it opens the database, so
the table only sat empty in every new database. The tests now pin that no
pause table exists.
* fix(native-chat): a Stop's pause never hides the restart pause
A Stop holds only the cards queued before it. The pause derivation still
returned the Stop alone whenever it was in force, so the restart pause was
never considered: a card queued after the Stop, written by a host process
that has since exited, sent by itself after Orca restarted, with no pause
header and no Resume. A /clear pause that held nothing could hide it the
same way.
Every pause in force is now derived. A card is held if any of them holds
it, and it names the first that does. The drain's pick, the consume
transaction's re-check and the published header all read that one rule;
the header names the pause holding the first card Resume would send.
* test(native-chat): pin the Stop's no-resend, lift and held-card rules
- The Claude and Codex Stop-withdraws-a-steer tests checked "not sent
again" at one instant, before a queue ignoring the pause re-sends. They
now wait for the stopped turn to end and re-check after a quiet window.
- The deleted-card test read a card queued after the Stop, which sends
whether or not a person's turn lifts it; it now reads the Stop's pause
before and after that turn.
- Unit cases pin that a Stop holds a card with no recorded position and one
queued before a rewind.
* refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller
The Stop row that paused the queue becomes the general Stop event
{ reason, turnId?, at, caller? }, whose reason is the host's existing stop
cause. It still rides a tombstone of a host-only id (a released host deletes
the journal from the first unknown row kind), and Resume keeps its own marker
on its own id. Only a person's Stop (reason user-stop) pauses the queue.
* test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event
* test(native-chat): pin that Stop and Resume rows never reach apps or count as history
* test(native-chat): only a person's Stop event pauses the queue
* test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop
Through the real host: the event names the turn and who asked and is in the
journal when the interrupt reaches the agent; at an agent still starting it is
there before the start is ended and holds a card queued before it; an idle Stop
writes one only when it withdrew a send; and the queue's claim re-judges a
pause that landed after its pick.
* test(native-chat): a card held at a starting agent is checked before the Stop's timing
Also says precisely what the claim's in-transaction pause check defends
against: the Stop and the drain share one serialized lane.
* test(native-chat): a released build keeps and folds a journal holding Stop events
Replays this build's rows from the released build's own journal database: every
row is kept, the history after the Stop still folds, and an older client is sent
only removed ids no item uses.
* style(native-chat): format the Stop event changes
* test(native-chat): type the released build's exports through one checked helper
* fix(native-chat): the Stop/Resume row guard narrows to those tombstones only
* test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade
The Stop-event downgrade test ran in no CI lane: unit shards exclude the
cross-version folder, and the cross-version lane runs a fixed file list that
did not name it. It is now on that list.
Its only case replayed the rows into a release's own fresh database, because
that release cannot open the current host database. A second case opens the
journal this build wrote with a main build that shares the database: it opens
writable, keeps every row, appends, and this build then reopens it with the
person's Stop still pausing the queue.
* fix(native-chat): a Stop that stops nothing new writes no Stop event
A Stop reaching a running agent wrote a Stop event on every press. Two
presses before the first interrupt landed wrote two events, so a card
queued between them counted as before the latest Stop and was held,
though a card queued after a Stop should send normally. A Stop naming a
turn that had already ended, as a phone sends late, also wrote an event
for a turn it never stopped.
It now writes one only when it withdrew a queued send, or stops something
no event records yet: not a turn the journal no longer runs, and not the
live turn a Stop still in force already names, unless a card was handed
over into it since, which this Stop's interrupt sends back and must hold.
The interrupt and the "already finished" note are unchanged. A Stop at a
starting agent still always writes.
* test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop
* chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled
* fix(native-chat): any later Stop event ends a person's Stop pause
A person's Stop paused the queue until their next accepted turn or Resume,
and a later Stop of another reason (the host stopping the agent, an
eviction, a close) was ignored. Now the pause is the latest Stop event's:
a later Stop of any reason ends a person's pause, and only a person's Stop
pauses. The fold keeps the latest Stop event whatever its reason.
An eviction of a resting chat writes no Stop event (a Stop that stops
nothing writes nothing), so it cannot release held cards; a test pins that
no event means no lift.
* fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed
A Stop pressed before the agent's turn shows in the journal (before
Claude's echo, or before Codex opens the turn) records no turn. A second
press once the turn showed compared that missing turn with the live one,
wrote a second Stop event, and held a card queued between the presses.
A repeat is now judged by what was sent since the Stop in force: with
nothing sent after it (a refused send aside), a Stop that named no turn,
or named the live one, is repeated and writes nothing. Anything sent since
and not refused, including a send whose fate is unknown, makes the new
press write, since its interrupt may send that card back to waiting.
Tests: the two-press case across the turn showing; a steer between the
presses settled unknown; and a Stop naming a turn that ended while the next
card is sent but shows no turn yet, which writes and holds that card. The
fold test that claimed an eviction path is renamed.
* fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes
A Stop or Resume row's value is read from disk with no shape check, and
the pause fold stored whatever it found. A stored `stopEvent: null` would
then throw on every pause check for that chat: the queue's pick, its
send, and every queue update to clients. No build writes such a row, so
this is hardening.
The fold now reads a Stop only when it is an object with a string reason
and a finite time, and a Resume only when it is `true`. Anything else is
ignored: it pauses nothing and ends nothing. The row is still not treated
as malformed, which could cut the history short.
* refactor(native-chat): one reading of a Stop's turn for its event and its note
A Stop's event and its note each worked out the same two facts on their own:
which turn the Stop is about (the one it named, else the one running), and
whether a named turn is the one the journal shows running. The event decides
before the interrupt; the note and whether the session ends decide after the
provider's answer, so those decisions stay separate, but the facts they read
are now one helper each in structured-agent-session-turn-stop-notes.ts:
structuredAgentSessionStoppedTurnId and
structuredAgentSessionStopNamesTurnNotLive. The event's turn, the note's key,
the session-ending condition, the running-command check and the repeat check
all read them. No behavior change.
Tests: a Stop naming no turn records the running turn on its event, and
rewrites that turn's note as a Stop naming it does.
* refactor(native-chat): a failed-interrupt Stop reads its turn through the shared helper
The new branch that ends a Codex child after a failed interrupt asked
whether the Stop's turn still runs with `turnId ?? liveTurnId`, a third
copy of "the turn a Stop is about". It now reads
structuredAgentSessionStoppedTurnId, the value the note key already uses,
read at the same point before the cancel. No behavior change.
Test: a Codex Stop whose interrupt failed ends the child, holds the card
queued before it with the queue paused, and writes its Stop event before
the turn's end.
|
||
|
|
477e699922 |
fix(codex): install Codex's Interrupt hook so an Esc-cancelled turn settles (#24332)
* fix(codex): install Codex's Interrupt hook so an Esc-cancelled turn settles Codex 0.150+ fires an Interrupt hook when the user presses Esc on an approval prompt or mid-tool, and nothing else. Orca did not install it, so the pane stayed blocked/working until the next prompt. - Add Interrupt to the managed Codex events and label maps, written with Codex's 3s cap (a larger value triggers a startup clamp warning). - Hash the timeout Codex hashes (Interrupt is clamped to [1,3], default 1) so self-computed trust matches Codex; pinned against a real 0.159.3 hash. - Map a root Interrupt to the existing cancelled-turn record (markCodexLeadTurnInterrupted), keeping child work in the fold; a child-scoped Interrupt is ignored. Relayed rows take the same path. * refactor(codex): let the hook builder own Codex's per-event timeout The managed hook's timeout is now Codex's own normalization of the shared budget, and every installer derives its trust entry from the hook it wrote, so no installer repeats the Interrupt special case. Claude-Session: codex-interrupt-hook review * refactor(codex): route Interrupt through the Stop lead update with an outcome Interrupt now writes the lead record through the same setCodexMainAgentTurnState call as Stop, so markCodexLeadTurnInterrupted keeps its original signature. Drops the child-scoped Interrupt guard: Codex never runs Interrupt hooks for subagents and its input schema has no agent_id. Claude-Session: codex-interrupt-hook review * fix(codex): ignore an Interrupt from an earlier turn once the next turn has started A replayed or late Interrupt carries the old turn's turn_id; matching it against the turn_id from the running turn's UserPromptSubmit keeps it from cancelling the new turn. Missing ids still cancel. * revert(codex): drop the Interrupt turn-id guard; delivery is already ordered Hook events reach Orca in order: Codex waits for the Interrupt hook before the next turn, and the restart spool is one append-only file per pane, replayed in order before live events. The guard protected an unreachable case and could drop a real cancel if the ids ever differed. |
||
|
|
b093d3ab20 |
feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)
orcad stops through slot-local and instance-bound request files, so a reused PID is never signalled. A managed stop is proven by its completion command and recorded as a receipt. Optional daemon retirement is best effort: an idle daemon retires, while a busy or unverifiable one stays up with its admission fence released. Runtime teardown runs in reverse order and keeps the instance lock and profile admission when any writer fails to stop. Browser discovery no longer delays readiness. Legacy worker recovery and watcher children are drained before the final flush. Headless terminal close no longer waits on a renderer tab that does not exist. No production deployment. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
ae41eb414a |
fix(terminal): give plain fish tabs Orca's codex function without changing fish's startup (#24284)
* fix(terminal): give plain fish tabs Orca's codex function without changing fish's startup A `codex` typed into a plain fish tab ran without --no-daemon because only wrapped fish tabs (startup command / ready marker) got Orca's codex function. Plain fish spawns now prepend an Orca data dir to XDG_DATA_DIRS and record the exact prefix in ORCA_FISH_XDG_DATA_DIRS_PREFIX. Fish sources the dir's fish/vendor_conf.d snippet, which first restores XDG_DATA_DIRS (unset again if it was unset), erases the marker, drops its dir from fish's derived vendor/function/ completion paths, then defines the shared fish codex function at the first prompt so the user's config.fish still wins. fish argv is unchanged; wrapped tabs keep their existing -C path. A local fallback to another shell restores the user's XDG_DATA_DIRS instead of deleting it. Bumps the terminal daemon protocol to v39 so new tabs move to a daemon that injects the env; v38 owners stay attachable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(fish): skip the XDG handoff for -N/--no-config and empty XDG_DATA_DIRS fish never reads vendor_conf.d under -N/--no-config (also abbreviated or clustered), so the snippet could not undo the prefix; and the restore cannot tell an empty XDG_DATA_DIRS from an unset one. Both now launch untouched. Run the real-fish handoff tests in the shell contracts job, where fish is required, so they no longer skip in CI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(fish): compare the unset-restore case against a fish without Orca Ubuntu runners ship snapd's fish vendor snippet, which sets XDG_DATA_DIRS on every fish start, so "unset" was never the right oracle there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(fish): treat an empty XDG_DATA_DIRS like unset so the tab still gets the codex hook Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(pty): put back the user's own launch env on a shell fallback The primary shell's launch config now records the pre-launch value of each key it writes. A fallback shell restores those values (unsetting keys that had none) instead of deleting the keys, which hands back an inherited XDG_DATA_DIRS after a fish fallback and an inherited ZDOTDIR after a zsh->bash fallback, with no per-shell special case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(fish): drop the Node restore twin and simplify the vendor snippet - Remove restoreFishXdgDataDirs; the generic fallback restore covers it. - Snippet: read ":$XDG_DATA_DIRS:" directly and filter Orca's vendor dirs with one string match per variable. - Require inheritedXdgDataDirs in both getShellLaunchConfig option shapes. - Drop the test-only FISH_XDG_DATA_DIRS_HANDOFF_DAEMON_PROTOCOL_VERSION. - Fix stale fish comments and trim redundant -N launch cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(fish): stop scrubbing fish's lookup paths after the handoff Only XDG_DATA_DIRS is restored, by exact prefix; Orca's dir holds nothing but this snippet, so leaving it on fish's derived paths loads nothing else and drops the glob match. * docs(fish): drop the comment for the removed vendor-dir cleanup --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
07dad6739a |
refactor(relay): sample fleet health inside the same-cap roll instead of a separate monitor run (#24443)
* refactor(relay): sample fleet health inside the same-cap roll instead of a separate monitor run A same-cap wave no longer consumes a 15-minute monitor dry-run and its sealed, single-use, five-minute-fresh evidence. Each apply wave now samples fleet health itself right before isolation, with the monitor's evaluator, thresholds, and tolerances, for a window sized to the cell's host count (3/5/8 min), plus three lookback rules: no cell container exit in 10 min, no minute over 500 director 503s in 10 min, and director concurrency p99 within the monitor bar over 4 min. Removes the monitor-run inputs, the gate's consume/authorize steps, the break-glass override, and the same-cap-only authorization shapes in relay-monitor-evidence.mjs. The monitor workflow and the rehome enable path are unchanged. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): bound the pre-drain sample overrun and keep the drain token fresh Review follow-ups: alternating tolerated readings could hold the sample open until its step timeout, so cap the overrun at three samples past the window; record why a read failed; mint a fresh admin ID token for the drain after the sample; raise the job timeout to 90 min so a long sample cannot cancel the job past the failsafe. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * feat(relay): exempt the rolled cell and existing-only cells from the pre-drain crash rule The exit rule counted every relay container exit fleet-wide, so a cell that crashes every few hours (c25, 12 a week) blocked the very roll that fixes it, and existing-only legacy cells (c5, 15 a week) blocked rolls they take no part in. Exits are now grouped by instance, each instance is named by its own newest runtime-metrics log line, and only exits on general or migration-only cells other than the target count. An exit no configured cell can be named for trips the rule; a failed lookup is a failed read. relay-observability.tf joins the evidence-code set because the rule depends on its filter. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * test(relay): cover re-asking for an unnamed exiting instance; note the boot-exit risk Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
051ca4d34e |
feat(relay): declare US cells c32 and c33 at the 3,000-host shape (#24444)
* feat(relay): declare US cells c32 and c33 at the 3,000-host shape Declares two us-central1 cells at the Asia shape (cap 3000, 6000 request units, e2-standard-4) with the US default pool of 10, and generalises the Asia topology and admission ladder to derive each wave's region from its reviewed zone, leaving every Asia wave's behaviour unchanged. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * docs(relay): note the US canary tie-break and leave the fleet pool list to promotion Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): plan C32 and C33 as one topology wave The live-image overlay refuses a declared non-target cell with no template, so a lone C32 plan would fail on C33. Registration and promotion stay one cell at a time. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
b82307937a |
fix(relay): admit drained hosts through their own lane and stagger their return (#24446)
* fix(relay): admit drained hosts through their own lane and stagger their return A same-cap roll's drain sends every host on the isolated cell back to the director at once. Those hosts reconnect through the sticky lane (one slot per director), and each one's re-placement holds that slot for most of a second behind the region-wide inventory lock, so ordinary reconnects time out behind them and the drained hosts retry every 2 s: ~30k 503s per drain. The reconnect verification read now also says whether the host's home cell is isolated for a roll right now (same predicate re-placement uses). Those hosts release the sticky slot after the read and take a separate drain-return lane (1 per director, matching the store's per-director placement serialization). When that lane is full the host gets a Retry-After that reserves the next free service slot, paced by the measured re-placement time and capped at 300 s. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): keep drain returns inside the placement pool budget and their own slot Review follow-ups for the drain-return lane: - The lane now borrows placement permits (never placement's last, never ahead of a queued placement), so placement + sticky still bounds the database pool. - A host's own early retry (row-busy redial, duplicate dial) gets the 2 s lane interval instead of a fresh slot behind the cohort, and a host that returns early to the same director keeps its reserved slot. - Classification also excludes an open migration row whose lease counter lapsed, matching the re-placement rule. - The load test now runs five directors behind random routing with a shared inventory lock. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
1a9ac0e955 |
feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)
Journal every orcad activation and rollback under a host fence so an interrupted one recovers to exactly the slot the activation record names. D7: planOrcadUpdate and assessOrcadRollback refuse a restart whose incoming build cannot attach the live terminal daemon's protocol. POSIX-only and inert: no production caller. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
8bf90ce2c2 |
test(e2e): give the sparse preset proof room to finish on CI (#24442)
The ~60-step screenshot proof takes 2-4 s per step on CI runners and hit the 120 s default on both failing nightlies, at different steps. |
||
|
|
9912042812 |
test(e2e): select the onboarding Codex card by its exact name (#24439)
#22720 dropped the command subtitle from onboarding agent cards, so the Codex card's accessible name is now "Codex" and /^Codex\s/ never matches. |
||
|
|
b3577b7c2a |
test(e2e): drag the manual-order worktree to a slot that changes the order (#24441)
Smart sort lists the new worktrees newest-first, so dropping the source before the row that already follows it is a no-op and correctly keeps Smart sort. |
||
|
|
197ea3a3b3 |
Free PR CI capacity by avoiding repeated setup and real-time test waits (#24355)
* Reduce repeated PR setup and transcript timing waits; add hosted comparisons * Align parallelism contract with Node-only external rebuild toolchain * Record hosted coverage and launch package, store, and cancellation comparisons * Apply hosted Windows setup savings and remove measured test waits * Keep measured PR package gains and remove completed comparison jobs * Report measured test counts with precise units |
||
|
|
99db2bfae4 |
feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)
* feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2)
Paired runtime environments gain a durable two-phase SSH access link
(prepare link, verified link, prepare unlink, complete unlink, cancel),
plus the reconciliation record type and the runtime identity verification
helper that T6 needs. Nothing calls the link store yet; T6's managed tunnel
and runtime SSH access are its first writers.
Why a sidecar: v1.4.217 and v1.4.218 parse orca-environments.json with plain
z.object schemas, which strip unknown keys, and rewrite the whole file on
routine use (markEnvironmentUsed). Storing the link there, as #16741 did, would
let a downgraded build keep the tunnel endpoint but drop sshAccess, stranding
the server unlinkable and losing its pinned host-key fingerprint. #16741's own
answer (bumping the store version) makes those builds reject the whole file.
So orca-environments.json keeps exactly its shipped shape (version 1, persisted
fields only), and all T5 state lives in orca-environment-sidecar.json beside
it: the link and its tunnel endpoint, the pending operation, the reconciliation
record, the verified runtime id and a monotonic pairing-revision floor. Reads
overlay the sidecar; each entry is bound to the environment's createdAt,
pairing revision and preferred endpoint, so a re-pair, removal or edit by an
older build makes it stale (ignored, pruned on the next sidecar write). An
unreadable sidecar fails closed, like the main file.
Porting note (source: #16741
|
||
|
|
198fe72066 | Update README downloads badge | ||
|
|
9bcdb6ad86 |
fix(native-chat): report a failed Stop child end through the host logger (#24437)
#24334 reported it through onEventSinkError, which #24312 replaced with the required diagnostics logger; main did not typecheck after both merged. |
||
|
|
56c7642aae |
test(orcad): skip the live-terminal runtime hand-over across a protocol bump (#24429)
* test(orcad): skip the Bun-to-Node live-terminal hand-over across a protocol bump The last Bun orcad's daemon reports protocol 38 forever, so asserting the adopted daemon matches this checkout's PROTOCOL_VERSION failed every bump. Ask the Bun slot's daemon for its protocol once, run the hand-over when it matches, and skip with the two versions named when it does not: a daemon at another protocol is never adopted across an update. * test(orcad): clean up the Bun protocol probe even when its launch fails The probe's cleanup ran only after a successful launch, so a launch that timed out or threw left its orcad and daemon running. One finally now stops the orcad, kills what it launched, and kills any daemon named by a pid file in the probe's data root. |
||
|
|
beccdec74c |
fix(native-chat): a Codex Stop that Codex refuses or never answers ends the Codex process (#24334)
* fix(codex): a Stop whose interrupt Codex refused or never answered ends the session A Codex Stop is the interrupt alone, so its background terminals keep running. When Codex refused the interrupt, or never answered it, the turn kept running and the chat said "Codex didn't stop" or "Cancellation was not confirmed." with no way to stop it short of closing the chat. Now, after an interrupt that failed, the Stop re-reads the conversation once Codex's frames already received have landed, and ends the child through the host's usual stop (proof of exit, then the lease release) when it still runs what the Stop was sent for. It skips a conversation at rest, a child whose exit already ended its turn, and one Codex has moved on to a different turn. The turn then reads as the user's cancellation. If the child's exit is not proven, the failure is reported and the Stop keeps its "didn't stop" or "not confirmed" row, which is still true. A Stop Codex took keeps the child, unchanged. * fix(codex): end the child only for an interrupt whose effect is unknown, on the turn the Stop meant - Codex's invalid-request refusal (-32600: no active turn, another turn active, thread not loaded) states the named turn is not running, and can arrive before that turn's end frame. The Codex adapter now marks it `turnNotRunning`, and such a refusal never ends the child. Only an internal-error refusal (-32603), an unanswered interrupt, or a thrown cancel does. - A Stop that named a turn, or an unnamed Stop that read one, ends the child only when the journal, after draining received frames, still shows that same turn. A session working on a follow-up whose turn has not opened is no longer ended by a Stop of the finished turn. - When the child's exit was proven and only a later cleanup step failed, the Stop reads as requested; the failure is still reported. - `AgentSessionCancelOutcome` moves beside the adapter's other Stop members (re-exported), which keeps the adapter file within its line budget. * test(codex): pin that a failed interrupt decides on the drained journal The frames Codex sent before the interrupt failed are held in the sink, so a read that skips the drain sees the stopped turn still running. * test(codex): a refusal naming a turn Codex is not running carries turnNotRunning * test(native-chat): fail the route release synchronously, as the adapter's acknowledgement is * docs(codex): a -32600 Codex could not parse also reads as not running and keeps the child * fix(native-chat): a named Stop whose child end is unproven says Codex didn't stop, not that the turn finished The new branch has just read the turn running, so the named Stop's 'already finished' row was false. |
||
|
|
34a582bd39 |
feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)
The relay gains an owner-reset surface that no current client calls. A session-owner client will be able to ask its relay to prepare a shutdown (`relay.reset`), have that preparation journaled durably beside the relay endpoint, and later recover it (`relay.recoverPreparedReset`, or the read-only `--read-reset-preparation` exec mode if the daemon is gone). Relay status advertises `relay.ownerReset.v1` and, when a journal is configured, `relay.durableResetPreparation.v1`. Why ahead of its caller: relays and clients update independently, so the host side has to be deployed before any client can rely on it. Old relays answer method-not-found and advertise neither capability, which a client reads as "unavailable", never as "reset". - relay-grace-lifecycle: shutdown is split into prepareShutdown and finishShutdown so a reset can settle its response before the process exits. Idle and signal shutdown keep today's behavior, including the deferred retry when disposal fails, and still do not wait on admitted requests; only a reset initiator drains work around owned-process disposal. - relay-work-drain-contract: both reset requests are admitted during a drain. - relay.ts / relay-daemon.ts: register the reset, its journal under `<endpointDir>/owner-reset-preparations`, the status capabilities, and the reader argv (checked after the self-test and Windows breakaway launch). Porting note (source: #16741 |
||
|
|
d53063d2b1 |
feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)
* feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) The SSH connection manager now registers each connection it allocates against the connection's transport-closure notice, and tracks every connect, disconnect, reconnect and teardown per target, so later slices can tell when a target's local work and transports are really gone. Ordinary connect, disconnect and quit behavior is unchanged apart from cleanup. Manager (ssh-connection-manager.ts): - registerConnection counts every connection a target allocated, retired pool entries included, until its transport-closure notice arrives. - connect/disconnect/reconnect/disconnectConnection/disconnectAll run through trackTargetOperation; a failed teardown is remembered as unconfirmed. - A failed startup is now disconnected, not just dropped from the pool. - disconnectAndDrain(targetId, signal) and disconnectConnection(..., drain), bounded by a 10s timeout. - disconnectAll(shouldDisconnect) only tears down targets the filter allows. IPC: - ssh-connect-attempt-registry: runSshTestConnectionProbe publishes a probe per target before it starts and clears testingTargets / credential flags only when the target's last probe settles. ssh:testConnection uses it, so quit still joins an in-flight probe within the shutdown budget. - ssh-target-lifecycle-queue: runTargetLifecycle returns the operation's value. - ssh-shutdown-drain: a mayDetach predicate (default: every target) is plumbed through detach, invalidation and disconnectAll. - ssh-renderer-broadcast: targets owned by a runtime are hidden by owner too, not only by id prefix. - ssh-target-registry: direct-authority resolver, installed by ssh-active-relay-sessions. - Provider dispatch: unregister*IfCurrent for git and filesystem providers. P8a, SSH provider continuations: ssh-provider-continuations tracks local settlement of SSH filesystem writes/deletes, imports, detected-worktree listings and worktree/folder removals per target. It records only local settlement, never remote execution or exit. Porting note (source: #16741 head |
||
|
|
35c8887a76 | fix(i18n): correct Korean working and shell labels (#24341) | ||
|
|
c6cfcc034e |
refactor(native-chat): structured chat failures always reach the diagnostics log (#24312)
* refactor(native-chat): give the structured chat host one required logger The structured chat runtime took an optional onError callback that the desktop never passed, so a late dispatch settlement, an unanswered-dispatch release, a journal event-sink write and a provider lifecycle delivery that failed were dropped with no trace. Other host failures went to scattered console.warn calls, which reach nothing in a packaged desktop build. The runtime and host now take one required logger (warn/error with a scope and fields). The production logger writes each entry as a failed span to <userData>/logs/main.trace.ndjson, which the diagnostic bundle collects, and to the console (stderr under a supervised headless host). The runtime and the host wrap it so a logger that throws never fails what it reports, and the install refuses without one. Sites that deliberately kept a recovery-capsule error out of the log still log no error object. * refactor(native-chat): hand the chat host's collaborators the logger, and give orcad its trace file The delivery loop, idle sweep, queued-message drain, lease renewer, event sink, conversation map and provider start/exit settlement each took an internal error callback that the host mapped onto the logger. They now take the logger itself and log under their own scope. The event sink keeps one onFailed hook, which decides whether to stop the provider, not whether to report. The dead-generation settlement returns its failure so each caller logs it under its own scope. orcad now installs the desktop's local trace sink under its own data root, so a headless host's chat failures reach <data-root>/logs/main.trace.ndjson as well as stderr. Also passes the logger in the test fixtures the first commit missed, which tc:node caught. * fix(native-chat): keep repeated chat failures from flooding the trace file, and record their causes - The production structured-chat logger writes a repeated failure (same level, scope, session, message and error text) once per 5 minutes, carrying how many repeats it swallowed; the tracked set is capped at 256. - Trace entries now carry the error's code (and SQLite errcode) and up to three causes by name and message. - A chat read whose conversation will not open is logged through the host's logger (open-for-read), and so are the runtime's chat-tab bookkeeping failures that already hold the host. - orcad writes its own orcad.trace.ndjson, closes it after every quit handler, and flushes it on process exit; a trace file that cannot be opened leaves tracing off instead of stopping the app or orcad. - Tests: the desktop wiring test proves the logger reaches the trace sink, and the privacy tests read every level the logger received. * fix(native-chat): log a created chat's tab-publication and launch-prompt failures through the host's logger * fix(native-chat): key a repeated chat failure on everything its entry writes The repeat suppression keyed on the message and the error's text, so two refusals with the same code but different causes, a plain error and a refusal of one code, or two object-valued errors shared a key and the second was swallowed for five minutes. The key is now the entry's whole written content (fields, code, errcode, refusal reason, cause chain, a stable rendering of a non-error value) plus the error's name and message; a refusal's reason is also written. * test(native-chat): pin that an error's name keeps two repeated failures apart * test(native-chat): build the refusal in the repeat-key test as the wire does * fix(native-chat): read an error's code and a refusal's reason by narrowing, not Reflect.get |
||
|
|
3fbdaba262 |
feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)
Adds the shared contracts for converting a desktop catalog into a dormant
managed-server catalog: the versioned migration manifest, import receipts,
dormant worktree/workspace/automation/session/client-state validation,
scrollback snapshot descriptors, migration preflight categories, catalog
state and staged-catalog normalization. Nothing calls them yet.
- The manifest is a cross-version artifact: it carries version 1 and any
other version is refused (orcad_migration_manifest_version_unsupported).
- Workspace references accept `folder:` keys alongside `worktree:` keys and
bare worktree ids; each must belong to the manifest's own catalog.
- Every list and payload is bounded: the manifest at 768 KiB, scrollback at
512 snapshots, each within the terminal store limit, 256 MiB in total and
fixed-size chunks.
- Workspace sessions are validated with main's own parseWorkspaceSession, so
the contract tracks the current session shape instead of a frozen copy.
Porting note (source: #16741
|
||
|
|
ece9e4d2e3 |
fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)
Splits runtime-environment subscriptions and the status probe out of their
IPC modules and fixes the reconnect and teardown races the split exposed.
Subscriptions (runtime-environment-subscriptions):
- A subscription id is reserved while its socket opens, so a duplicate request
for the same id is refused instead of racing the first.
- Every callback checks it still owns its id by token, so a late event or close
from a superseded socket cannot reach a subscription that reused the id.
- A close that arrives during setup closes the new socket and fails the call
instead of registering a subscription that is already dead.
Status (runtime-environment-status-probe, status-owner):
- A status request records the capability incarnation it started under; if the
environment was re-paired or retired meanwhile, it answers
runtime_environment_changed instead of publishing stale status, diagnostics or
a runtime id.
- runtimeEnvironmentChangedFailure moves to the revision guard and
shouldUseSharedControlEnvelope to support routing, so both have one home.
Status and transport routing still resolve environments with plain
resolveEnvironment; the managed-tunnel hook is T6.
Porting note (source: #16741
|
||
|
|
dd87ae578d |
feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)
- orcad-remote-runtime-control: the one launch-and-poll-readiness loop that
deployOrcad and rollbackOrcad now share, plus exec and recovery helpers.
- orcad-remote-record-file: bounded, marker-delimited reads and atomic writes
for host records. A read that returns no verifiable answer rejects instead
of reading as "no record".
- The activation record store reads through it (a lost read no longer becomes
an empty record) and writeOrcadActivationRecord refuses to replace a record
this client cannot read, such as a newer schema; deploy and rollback use it.
- orcad-active-readiness: prove a recorded-active or relaunched slot against
the activation gate, reporting exited / unverifiable / rejected.
- orcad-remote-build-hash and orcad-remote-context (host, home, server target
via orcad-deployment-target, activation record; POSIX-only).
- Readiness reads are capped at 256 KiB, and a finished invalid JSON line is
malformed rather than pending forever.
Inert: nothing in the app calls managed orcad deploy yet. Ported from #16741
(
|
||
|
|
92cb71765e |
feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)
* feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6)
P6: a relay now answers pty.resumeClient, which admits only an exact resume of
the existing session owner (it never mints a fresh claim when that owner is
gone). resumeSshPtyConsumerSession calls it with cancellation and authority
checks; an old relay's method-not-found becomes a pty_consumer_resume_unsupported
refusal that leaves the channel usable for pty.openClient. Owner grant
publication now rolls back if the response-settlement hook cannot be armed, and
the adapter exposes read-only owner and publication-settled queries.
P5: SshPtyProvider.listProcesses moves to ssh-pty-process-list unchanged, and
the notification-routing tests split into a shared fixture plus recovery and
recovery-activation files.
Nothing calls pty.resumeClient yet (T6). Ported by hunk from #16741
(
|
||
|
|
ff212dbbef |
feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)
* feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b)
- DaemonPtyRouter routes idle retirement through DaemonRouterRetirement: it
fences new spawns, counts spawns already in flight, takes a census of every
daemon generation and retires them only when all are idle. A lost reply
keeps the fence; an unanswered census is unverifiable, never empty.
- Each adapter answers requestIdleRetirement through shutdownIfIdle and fences
its own spawns while retiring.
- A recovery-only adapter/provider (createDaemonRecoveryProvider) reattaches
and controls existing sessions but admits no new process, never respawns
the daemon, and never prunes sessions of unknown worktrees.
- listLiveDaemonSessions and requestIdleDaemonRetirement report null /
'unverifiable' when any generation cannot answer.
- ptySpawnHealth replies add optional coverage and Node runtime fields;
checkDaemonHealthWithCoverage reads them and treats an absent coverage from
an older Windows daemon as handshake-only.
- reconcileOnStartup moves to daemon-router-session-reconciliation unchanged.
No caller uses retirement, the census or the recovery provider yet (T6), so
they are inert. Ported by hunk from #16741 (
|
||
|
|
d23ecef301 |
feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)
* feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9)
The renderer's session write subscriber now waits for the local session patch
to be accepted by main. While a write is in flight no second write starts; if
it rejects, the fields it carried go back into the pending set (newer edits to
the same fields are not overwritten) and the next store or gate wake retries
them instead of silently dropping them.
Boot PTY hydration verifies a folder workspace as local only when its id is
unique and it is pinned local, or its legacy scope resolves local with no
remote candidate repo. A folder under an SSH project group is never verified.
Ported by hunk from #16741 (
|
||
|
|
6b36e4f85b |
feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)
* feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1)
Relay shutdown and the runtime watcher pool now wait for the child processes
they own to physically exit instead of only signalling them:
- WatcherProcessSupervisor tracks launched children and exposes
disposeAndWait; RuntimeWatcherProcessPool retires slots through
RuntimeWatcherDisposalOwners, which retries failed disposals.
- The relay watch registry fences new watches once disposed and joins
in-flight native unsubscribes; FsHandler.disposeWatchers awaits it.
- AgentExecHandler tracks spawned agent children until they close, so a
request timeout is no longer taken as proof the child is gone.
- RelayRuntimeServices.disposeOwnedProcesses awaits both, alongside the
existing stream drains; a failure defers shutdown for the grace retry.
Ported by hunk from #16741 (
|
||
|
|
e42c0b3630 |
fix(orchestration): call the Orca session ID orca_session_id everywhere agents see it (#24230)
* refactor(orchestration): spell the Orca session address orca_session_id:<id> The database already names this identity orca_session_id, but agents saw and typed session:<id>, which collides with the Claude/Codex session ID, Session History and terminal sessions. The prefix is now orca_session_id:. No alias, migration or version handshake for the old session: spelling: restructured native chat is experimental. The coordinator-address triggers compile the prefix into their body, and migrate-v42 stamped them once, so an existing database would keep remembering session:<id> for new Runs. They are now recreated on every open, like the mail routing trigger. Provider-id refusals now name the "Orca session ID" instead of "Orca address"/"Orca id". * fix(orchestration): name the Orca session ID the same way in both provider-id refusals The send refusal handed back orca_session_id:<id> as the Orca session ID while the caller refusal called the bare id by that name. Both now say "This session's Orca session ID is orca_session_id:<id>"; the caller refusal also names the bare value ORCA_AGENT_SESSION_ID accepts, since that input takes only the bare id. Tests pin both refusal texts, that session:<id> no longer parses as a session, and that the older-build trigger rewrite actually changes the trigger SQL. * fix(orchestration): keep chats reachable at their session:<id> address after the rename Existing native chats were addressed as session:<id>, so agents and stored mail still use that spelling. Input now accepts session:<id> and treats it as orca_session_id:<id> (the codec parses both; nothing writes the old one), and schema v43 rewrites every stored session:<id> address once: message senders and recipients, remembered Run coordinator addresses, structured pointer operation keys, and coordinator loop handles. Subjects, bodies and payloads are left as written. * fix(orchestration): refuse session:<id> input again; stored addresses still migrate once The old prefix is not accepted as input: no respelling helper, no special case. A session:<id> recipient is refused like any unknown terminal handle (terminal_not_found), including after v43 rewrote a Run's remembered session:<id> coordinator address, so the old spelling no longer routes. Schema v43 still rewrites stored session:<id> addresses once. |
||
|
|
7176648759 |
fix(native-chat): every chat action press is its own action (re-land #23916 on main) (#24301)
* refactor(native-chat): a stopped child ends on the one reading of its stop
The eviction step reads a stop's result through `stopAgentSessionProviderRoot` and hands that
verdict to the child's ending, so the host never forms a second view of whether the root is gone.
Every ending carries it: a stop's comes from that reading, an exit's root is gone by definition,
and a failed re-attach passes what its release saw. The end-of-child record can therefore also
carry a stop whose root was not seen to go, which nothing ends on yet.
* feat(native-chat): the host says it accepts a send before any agent has it
The host now lists agent-session.accepted-send.v1 among its own runtime capabilities, the same
string capable clients already send. A client can then tell a host that answers a send at
acceptance, and admits a Stop with no writer before a turn starts, from an older one that still
restarts the agent inside the send. Additive: an older client ignores a capability it does not
know.
* refactor(native-chat): an attach never opens a journal of its own
The attach adopts the conversation's open journal, which outlives it, so it no longer opens one
for a direct caller either. That leaves nothing for a failed adopted import to close, and the flag
that told the two cases apart is gone. Tests that attach without a host open the conversation the
way a host does.
* fix(native-chat): a moved fence resends nothing on a host that accepts first
The outbox treated any fence change as a new owner: it dropped the answer of a send in flight,
queued that send to go out again under the same id, and unblocked a refused head. On an older
host that is how a send the restart refused, unrecorded, gets another try. On a host that records
every send before it starts an agent, a fence moves because that start ran, so the same rule
resent into every failed start. With a fence stamped on every frame, that became a loop.
The outbox now reacts to a fence change only when the host has not advertised that it accepts a
send before any agent has it. On such a host, only a Retry or a new send goes out, and a failed
start reaches the client as a rejected message it keeps with its Retry. Against an older host, or
before one has answered, the outbox behaves as it did. Desktop and paired web share this hook.
* refactor(native-chat): a child's end says whether the user or the host stopped it
The end-of-child record's cause now tells a user's Stop from the host stopping the child for a
cause of its own: `user-stop` and `host-stop` replace `stop`. The delivery loop goes on after a
user's Stop, as before, and fails the start it was waiting on after a host stop, with the one
error row and every queued message rejected, in the stop's reason when it gave one. The reason
stays description only. Stop passes `user-stop`; nothing passes `host-stop` yet.
* fix(native-chat): a chat whose only work is a queued message is not offered for resume
A message accepted while the agent was starting counts as working in the chat, and quit rejects it
as never sent. The teardown snapshot read the same working rule, so a relaunch offered to resume a
chat whose agent never had the message. The snapshot now reads only what was handed over.
* fix(native-chat): the conversation outlives its agent
Opening a chat no longer starts its agent. A conversation is reached through one host
accessor that opens its journal at rest, and a send is what starts the agent, through
the delivery loop. One idle sweep, every five minutes, stops an agent that has been
quiet for thirty minutes and owes no work, then drops an open journal handle that is
only a cache. Its record, tab, status row and readers stay.
- hold and release are no-ops; hold still builds the host for shipped mobile builds.
- The holders, the holds, the release clock and the exit respawn are deleted.
- Options, the model list, the goal and the context meter answer at rest; a model pick
at rest is recorded as intent for the next start.
- Compact, rewind, clear and goal changes start the agent first. A send does too when
a rewind is still in doubt after the conversation opens.
- Orchestration routes mail and group addresses on ownership (the record plus the chat
tab), not on whether the process runs. An open dispatch keeps its worker running.
- The restart continuation is a send; Resume all holds each slot until the message is
handed over or rejected.
- A read error never replaces a loaded transcript, and shows the host's own words.
* test(native-chat): type the queued-message fixtures in the resume-offer tests
* fix(native-chat): a start that dies while a message waits on it is that message's failed start
Opening a chat's tab starts an agent for the view, and a send accepted meanwhile waits on it. When
that start died, its exit wrote the start's error row and left the message queued, so the delivery
loop started a second agent into the same failure and wrote a second row. A child's end now records
where the conversation's journal stood, and the loop settles a message accepted before a failed
start ended with that start: one row, under its key, and no second start. A message sent after the
failure still gets a fresh start.
* fix(native-chat): a request that failed reads as failed
A structured chat whose only message the agent's start refused read as a
green finish, and a cancelled structured turn did too: the host published a
verdict only for turn records, and structured rows carried no `interrupted`.
The host projection now reads the session's latest request: its turn's
outcome, or `failure` for a send the agent or its start refused. A send
that was withdrawn, or left undelivered by a restart or a close, fails
nobody and makes nothing listable. The ingest publishes `interrupted` as the
hook lanes do, and every reader decodes the verdict through one accessor, so
a failure reads Failed on the dot, the rollups, history and `worktree ps`,
behaves like a cancellation in every clean-finish policy, and notifies as
"failed".
* docs(native-chat): say what an attach's open conversation and unconfirmed ids are now
* test(native-chat): a verdict change republishes the mobile status projection
* refactor(native-chat): the store's retention trigger keeps its flag compare
A verdict change always moves the completion clock the same check already
reads, so a second verdict compare there caught nothing new.
* test(native-chat): a user message the provider journaled keeps its session listed
* test(native-chat): pin what a failed start settles, and what a resume offer names
A view's child that dies while a sent message waits settles that message only when it died starting
and no child has taken its place: a proven child's crash, or a second start since, gets the message
delivered. The resume offer names the handed-over message, never a newer one still queued.
* test(native-chat): the failed-start pins fail on what the message became, not on a timeout
* fix(native-chat): a restart offer ends when the chat's agent starts again
The offer used to end only when the chat's newest user message changed,
because opening a chat started its agent and that start could not be told
apart from real activity. Opening a chat starts nothing now, so the host
reads the fact it already publishes: a chat's status row goes from not
host-owned to host-owned exactly when its agent is started. At that edge the
offer and any failure record for the chat are withdrawn, unless the start is
a resume action's own (its continuation is the oldest undelivered message).
A continuation and a message racing to be first are decided at acceptance:
the continuation is refused, quietly and with nothing filed, when any other
message was accepted since the restart. A failed continuation start leaves
the offer retryable, and each resume action sends its own message id.
Deleted: the newest-user-message comparison, its journal reader, the
continuation filter, and the failure ledger's own "answered by the chat"
check. The marker still carries its message id for one release, so the
previous build can read it.
* fix(runtime): end a transcript stream when its client unsubscribes
Desktop: the IPC subscription controller was dropped as soon as the streaming
handler returned, which for most streams is right after it binds. A later
runtime:unsubscribe then found nothing to abort, so the host kept the subscriber
and derived and sent every publish to a channel no one listened to. The controller
now lives until the renderer unsubscribes, resubscribes the same id, or goes away.
Mobile: disposing an agentSession.subscribe stream now sends agentSession.unsubscribe
with the stream's frame id, so the host ends that subscriber and leaves a sibling
stream on the same socket running. The direct path now passes the frame id the relay
path already passed.
* fix(native-chat): a late provider-session update keeps a failed recovery record failed
A provider-session heartbeat that rewrites a completed recovery record kept
its interrupted flag but dropped the outcome it was copied with, so a live
failed checkpoint read as a clean finish until the next status write.
* test(orchestration): the preamble's host stub is typed, not cast
The preamble send now takes only what it reads of the host, the send, the settlement wait and the
record's fence, so its test builds that host with real types instead of `as never`.
* test(native-chat): the terminal-bell check asserts the renamed verdict field
The bell notification test still checked for agentInterrupted, which no
longer exists, so it could not catch a verdict leaking into a bell dispatch.
* fix(native-chat): a failed turn ranks like a completion for attention
Attention readers (completion time, Smart Sort, sticky retention, Cmd+J
Recent) now demote only a turn the user stopped. A failure is news the
user has not seen, so it keeps its completion time, ranks in the Done
class, stays retained after its pane goes away, and a retained failure
reads failed in the worktree rollup instead of done. Clean-finish
policy (hibernation, pane ownership, the value moment) still treats a
failure like a stop.
The retention trigger compares verdicts again: success -> failure no
longer moves the completion clock.
* fix(native-chat): one fact ends a restart offer: the chat moved on since the restart
The offer is live while no other message has been accepted in the chat since the
restart and its agent has not proved a start since. The offer list, the resume's
reservation check and the continuation's acceptance check all read that one fact,
so a message whose start then failed withdraws the offer too, and a stale click
finds nothing to act on.
The fact is read off the conversation's open handle, which the restart closed, so
it is retired durably whenever it may have changed: a message accepted, a start
proven. A close and reopen within the same run therefore cannot bring the offer
back. A continuation rejected before it reached the agent does not count, so a
retry after a failed start still runs.
Deleted: the quit-time gate on withdrawal, which changed nothing because the
withdrawal and the quit's own offer write share one queue; the per-action
"withdrawn" flag and the separate acceptance check it paired with.
* test(native-chat): an older build reads the restart offer this build records
The offer lives in a file the previous release reads after a downgrade. Pin that
against the pinned release's own capsule, and run the lane when the marker or the
capsule changes.
* fix(native-chat): read a restart offer against where the journal stood when it was taken
"Since the restart" was read off the conversation's open handle, which the idle
sweep closes: after a reopen, a message the user had already sent looked older
than the handle and the withdrawn offer came back.
The offer now records the journal position (epoch and sequence) at the moment
it is taken, and a message accepted after that position, or a journal on another
epoch, means the chat moved on. That is derived from the journal, so it holds
across any number of closes and reopens. An older build's offer has no position;
only a start withdraws it. Because the message half is now durable, the offer is
no longer rewritten in the recovery file on every accepted message; a proven
start still writes it, since only the host that saw the start knows of it.
* test(native-chat): wait for the listing's retire write before reading the recovery file
* refactor(native-chat): every journal row states which turn it belongs to
Rows gain a turn scope stated by the write that creates them: the open root
turn, or the conversation. A queued message takes its scope from its handover.
Rows stored before scopes existed are placed on replay by the root turn open
when they were created, so no persisted state is needed for them. Rewind keeps
each retained row's scope and producer, so a subagent's row stays its own.
* fix(native-chat): keep the terminal-backed chat's read error over its local echoes
Messages winning over a read error is right for the structured chat, whose read retries and whose
messages came from the transcript. The terminal-backed view assembles its list from local echoes
too (a launch prompt, a pending send), so a failed read there showed only those bubbles and no
error. Only the structured pane now keeps messages over an error.
* fix(native-chat): a start retries the exit settlement a failed journal write left owed
An agent exit whose journal settlement write failed releases the lease latched until a retry lands.
Reopening the chat used to be that retry; with reveal now only opening the journal, nothing retried
it before the next app launch, and every send was refused. The start the send needs now runs the
retry first, where the attach would.
* fix(native-chat): a failed main agent reads failed while its subagents still work
The verdict is now read from the main agent's own state, not the folded
row: a main agent that is done and failed has a verdict even while its
subagents keep the row working. Without mainAgent (history, worktree ps,
older hosts) the old combined-done rule stands.
Display marks the verdict through agentVerdictDisplayMark: a failure
outranks every combined state on the agent's dot, label, tab badge,
dashboard and activity rows; a stop marks only a done row, so a
successful or stopped main agent with live subagents still reads
working. Subagent rows keep their own state. The worktree card, terminal
tab and Cmd+J rollups share one pane fold and rank a pending question,
then failed, then working, monitoring, interrupted and done.
worktree ps publishes the main agent's outcome on a working row, and the
mobile mirror reads it. The store's change check, the paired-client
mirror's equality and its epoch now see a verdict change on a working
row, which otherwise moves no state or clock and left the worktree card
reading working. Clean-finish policy is unchanged: a working row is never
hibernated and has no completion time.
* perf(native-chat): answer the owner check without opening the chat
Worktree activation calls agentSession.handoffStatus for every chat tab in the worktree, and the
answer comes from the session record alone. Reaching it through the accessor opened each resting
chat's journal (a full read, the crash-boundary write and a restored status publish), then kept it
open for the idle window. It now checks the record and the adapter's support, as before this series,
and opens nothing.
* fix(native-chat): a read waiting on the session lock opens nothing once quit began
The accessor checked for quit before queueing the open, so a read queued behind a session task ran
its open after teardown had begun and indexed a journal no teardown step would close. The check now
runs at the open itself.
* test(native-chat): pin stated turn scopes, the upcast of unscoped rows, and rewind attribution
* fix(native-chat): /compact is a message the chat sends, run as a turn of its own
The conversation command RPC now accepts /compact into the queue like any
send and answers once it is handed over. The delivery loop opens the command's
own turn, starts the provider on it, and waits for the provider's end off the
session's queue, so messages typed meanwhile are held and delivered after it,
even when it fails. It settles by re-reading the journal: a child that died
meanwhile already wrote the verdict. Stop ends the command at once. The 180 s
completion window, the unconfirmed row and the recovery of an older build's
compaction record are gone; that record no longer gates anything. On Codex the
provider turn the command opens is claimed into the command's turn.
* fix(native-chat): read a failed resume's chat before calling it retryable
Whether a failed resume is retryable is the offer's own rule: the chat has not moved on since the
restart, read from its journal. The failure list read it only for a chat already open, so once the
idle sweep closed a chat the user had moved on in, its failure showed Retry again, and the click did
nothing. The list now opens the failed chats first, as the offer list does.
* test(native-chat): type the provider event sink the settlement test reaches for
* docs(native-chat): the worktree ps outcome comment no longer claims old hosts send it
The field is new: an old host sends no outcome at all, so a reader falls
back to interrupted. The removed clause said old hosts send it on done
rows, which never shipped.
* fix(native-chat): say the structured read keeps trying only where it does
The structured pane's "Orca keeps trying to load it" line never showed: the view state filled in an
untranslated fallback whenever the read error had no text, and the empty state prefers any message.
The view state now leaves the message out, so the structured pane shows that line and the
terminal-backed pane its own translated one. Mobile's structured lane does not resubscribe after an
error frame, so it no longer makes the claim.
* fix(native-chat): rows group under the turn their record names, not the one above them
Each row's turn is the turn its stated scope names, anchored on the entry
that opened it, or on the turn itself when the provider opened it unasked.
So /compact groups its own rows and the previous turn is untouched, a message
typed into a running turn joins it, and a provider-resumed turn folds under
its own Worked-for. A row reporting how a turn ended, an error or the
compaction separator, never folds. Desktop and mobile read the same keys; a
host that states no scope keeps today's positional grouping.
* test(native-chat): await the send's settlement instead of polling for the start
The at-rest send tests polled for the provider start with vi.waitFor's one-second default, which a
loaded machine outran. They now await the host's own settlement of the message.
* docs(native-chat): the status-store listing rule names provider-journaled user messages
* fix(native-chat): a restart offer resumes any time after the quit, and knows its own continuations
The continuation's message id was dated by the quit, and the ledger refuses a new id dated more than
a day back, so Resume or Retry a day after quitting was always refused (on main too). It is now
dated by the resume action.
Telling a rejected continuation from the user's own message read the operation ledger, whose rows
expire after about a day; after that a failed resume stopped being retryable. The offer now
records the continuation each action sends on its own capsule entry, bounded to the newest 16, so
the ids end with the offer. The ledger read is deleted.
* fix(native-chat): a /compact is not a request the sidebar, notifications or restart resume report
The sidebar's prompt, preview, verdict and instant, the turn-completion feed,
and the restart-resume marker read past a conversation command and its turn to
the last real request, so a /compact neither notifies nor re-dates the row,
and a command in flight is never offered as work to resume. An older client
shown a command's turn in the legacy form names the session's own agent.
* fix(orchestration): route no mail to a structured worker its orchestration released
A structured worker is routed on ownership, and a resting worker's lease is released, so ownership
held while its chat tab stayed listed. A worker the coordinator abandoned and then released, found
at rest by the release, therefore still took peer mail and @worktree: broadcasts, and each one
restarted its agent. Routing now also reads the orchestration's own resource row: once it is
released, direct mail, group addressing and worker-show's addressable answer drop the worker, as
they would a terminal worker whose terminal closed. The chat tab stays, and nothing new is stored.
* fix(native-chat): a failed retry names the user's prompt, not Orca's continuation
A resume's continuation is written to the chat before its start, so after a failed attempt the chat's
newest user message is that rejected continuation. A second failure then showed Orca's own restart
text as the chat's prompt. A retry now keeps the prompt its first failure named.
* test(native-chat): pin what a conversation command's admission refuses at rest and at handover
* test(native-chat): tests merged from the base state which turn their rows belong to
* fix(native-chat): a refused send notifies failed through the completion feed
The host's completion feed followed only the newest turn, so a send the
agent or its start refused, which creates no turn, read Failed on its row
but sent no notification. The feed now follows the session's latest
request, read from the projection the status feed already makes for the
commit: a turn keeps its id, a refused send is named by its journal item
key. It announces only while the session is idle, as the row reports a
verdict, so queued sends refused one commit at a time notify once, and a
withdrawn send falls back to a request already announced.
* fix(orchestration): read the released row optionally, as the authority does
worker-show's observation called the row lookup directly, which a runtime double without it threw on
and failed the structured tab-retirement release.
* chore(native-chat): one import per module and no unexplained casts in the turn-scope changes
* test(claude): pin which turn a Claude row joins, including a subagent's after the turn ends
* fix(native-chat): the status bar drops a restart offer the chat moved on from
The renderer re-read the host's restart offer only when a failed chat showed activity, so after a
message withdrew a pending offer the host answered no chats while the status bar kept counting one,
and clicking it opened nothing. The same watch now covers pending offers: a status change in an
offered chat asks the host again, once.
* fix(native-chat): a refused steer is read from the turn its handover named
The latest-request reader decided whether a refused send had joined a running turn by comparing
host clocks: its handover time against the previous turn's end. The handover row now states the
turn it delivered into, so the reader reads that instead and the clock comparison goes. A journal
written before handover rows stated a turn is scoped on replay from the turn open when each row
was written, which can differ from the clock reading only when a send and a turn's end share a
millisecond.
* fix(mobile): the native-chat controller contract carries the turn journal
The controller and overlay already pass nativeChatTurnJournal, but the
contract type never declared it, so mobile failed to typecheck.
* fix(native-chat): the live turn is the running turn, not the newest user row
A turn the provider opened on its own (a background wake, a resumed turn)
anchors on its own record, but the list still treated the newest user row
as the live turn. While such a turn ran, the settled user turn before it
lost its duration and the running turn's own rows were drawn as settled,
so its tool calls lost their live state.
nativeChatTurnMembership now answers both questions from the turn record:
each row's turn, and the live turn (the running root turn's anchor, else
the newest user row, which is also all an unscoped host has). Desktop and
mobile key liveness, the timing clock and the live status's row on it.
* test(native-chat): a turn the provider opened keeps its own clock
Pins that the local turn clock follows the live turn, so a wake after a
settled turn does not restart that turn's clock when no host durations
are recorded.
* fix(native-chat): a running turn no message opened draws its status on no row
Its live status belongs to the transcript-tail indicator alone. Once it
settles, its duration draws at its first row as before; a running turn a
message opened still draws on that message.
* fix(native-chat): every copy of a row carries the main agent's own status
History entries, sleep records and `worktree ps` rows carried a flattened
top-level `outcome`, copied under different gates and without the main agent's
clock. They now carry `mainAgent` (state, outcome, stateStartedAt), the type
the live row already persists and sends, and every copy site takes it with
`interrupted` through one function, `agentVerdictFields`.
- The accessor reads `mainAgent` then the legacy flag; the mobile mirror
matches it line for line.
- Sleep records admit `mainAgent` with `normalizeMainAgentStatusField`, so a
malformed value drops the field, never the record.
- Mobile dates a main agent that failed under live subagents by its own clock,
as desktop does, and its row equality compares `mainAgent`.
- The activity feed reads a history entry's own `mainAgent` instead of
rebuilding one; the sync key and history equality compare it.
* test(native-chat): pin the worktree ps verdict across host and phone versions
Pairs the real v1.4.212 host and phone row reader with this build: an old phone
reads a new host's rows by `interrupted`, a new phone reads an old host's rows
(no `mainAgent`) the same way, and a new phone reads a failure under live
subagents as Failed, dated by `mainAgent.stateStartedAt`. The release checkout
now carries the phone's self-contained row reader, and the lane runs when the
`worktree ps` row producers change.
* test(mobile): name the parity table's row for its role
* test(native-chat): a roster of idle or finished children does not keep an agent awake
The sweep reads owed background work through the shared child-work liveness that upstream's
release clock adopted; a child that went idle or finished is not work the agent still owes.
* fix(native-chat): a request that settles while the user is asked something notifies once
The completion edge waited for an idle session, and a pending prompt (including a
subagent's approval) is not idle. Structured chat has no other attention producer,
so a main turn that finished while a subagent waited on the user sent nothing
until the prompt was answered.
The edge now waits only on owed work (a running turn or an unanswered send), which
the projection reports even beneath a pending prompt. A request that settles with
a prompt pending announces once; the renderer words it "needs input" from the
host status mirror's `attention`, and answering the prompt keeps the same request
identity, so it does not announce again. The wire shape is unchanged.
* fix(orchestration): a task dispatched into a resting structured worker keeps it running
The sweep's open-dispatch check read only the worker-start dispatch that owns the worker's terminal
resource, so a task later dispatched to the same worker (orchestration dispatch --to, which writes a
dispatch with no worker row) did not count: after thirty quiet minutes the worker was stopped while
that task was open, and its coordinator read exited. Any unsettled dispatch addressed to the worker's
process incarnation now counts, derived from the existing rows.
* fix(native-chat): a command's wait ends when its child does
The delivery loop waited for a /compact only on the adapter's compaction
tracker, which learns of the child's end only on some exit paths: a Codex
exit or close, and a Claude close, never reach it. The wait then never
ended, so nothing queued behind the command was delivered again, Stop had
no child to answer through, and the tracker's leftover entry refused the
next /compact.
Every way a child ends passes endProviderChild, so the host now offers a
per-child end signal there. The loop races the tracker against it (the
dead-generation settlement has already written the command's verdict),
and on that end asks every adapter to release the command, so a later
command runs and no later provider turn is claimed into the dead one.
The adapters' own exit-time releases were unreachable (Codex) or covered
one path of several (Claude), and are removed.
The Codex RPC test harness moves to its own module so the exit can be
driven through the real adapter's connection callback.
* fix(native-chat): keep refusing sends during a command on an older host
An older host's controller still refuses a send while a conversation
command runs, so dropping the client's block turned every message typed
during /compact into a 'not sent' row with Retry there. The block stays
for hosts that do not run the command as a send-path turn, and goes only
for those that do.
The signal is one the client already holds: a host that runs /compact on
the send path states a turn scope on every journal row it writes, the
same fact turn membership uses to tell it from an older host. Both now
read it from one predicate. On an empty conversation, or one whose rows
all predate the upgrade, the signal is absent until the command's own
entry streams in, so that brief window keeps the old local refusal; no
capability or wire field is added.
* docs(native-chat): comments stop describing the hold this PR removed
Eight comments still justified orderings and teardown choices by a viewer or dispatch hold that
pinned the provider child. Nothing holds any more; the orderings stand for the binding's redrive
subscription and parked mail, and a chat's agent runs from a send until the idle sweep rests it.
Comment-only.
* fix(native-chat): the completion says when the user is being asked
A request that settles while a prompt waits on the user was worded "needs input"
from the renderer's status-feed mirror. Remote clients receive the status and
completion streams over separate sockets, so they can arrive in either order and
the wording could be wrong both ways.
The host already knows at emit time, so the completion now carries an optional
`awaitingUser: true` in that case and omits it otherwise. The renderer words the
notification from that field alone and no longer reads the status mirror. Old
clients ignore the field and word by outcome; old hosts never send it.
* fix(native-chat): a restart offer keeps the start its own continuation made
Whose start ended an offer was decided at read time, from whether the offer's continuation was
still the queued message. Once the provider refused that continuation, the child it had started
read as someone else's start, so the offer ended and its failure showed no Retry. The delivery
loop now records which queued message a start is for on the in-memory child, and the child's end
carries it; the offer counts a start as its own when that message is one of its continuations.
* fix(native-chat): a rewound turn still names the message that opened it
A Codex rewind rebuilds the epoch without submissions, so each sent message survives only under
its provider key. The kept turn records still named the submission key, so each turn anchored on
itself and its rows grouped apart from the message that opened it. The rewind now renames the
turn's opener along with the message.
* fix(native-chat): Stop ends only the command it names
Stop on a command turn abandoned whatever compaction the session had pending, so a late Stop for
an earlier /compact cancelled the one running now. The tracker now ends a command only when the
Stop names its turn, and the cancel reply reports whether it did.
* fix(native-chat): an agent gets a full idle window after its owed work ends
The sweep measured quiet only from the last journal row, so once a subagent, command, monitor or
dispatch that had outlived the window ended, the agent was stopped at the next tick. A child can
read done before the lead's wake-up turn writes anything, and stopping in that gap loses the
wake-up. The sweep now counts owed work it observes as activity, which gives the agent the full
window afterwards, as the release clock it replaced did.
* test(claude): the options-read fixture runs a live child
The fixture marked its conversation running with a hasProviderChild field the
session type does not have, so the read took the at-rest path and refused a
session with no record. It now carries a child, which is what the read checks.
* test(native-chat): host tests reach its collaborators through a typed seam
The rest-test rig and three test files read the host's private members with
Reflect.get and cast the result. The host now exposes one test-only accessor,
collaboratorsForTests(), and the subscribers class a subscriberCountForTests()
beside its existing retainedActivityCountForTests(), so the tests are checked
against the real types and the casts are gone.
* fix(worktree-status): a departed agent's failure yields to live work on the worktree card
A retained failed agent has no expiry, so ranking it with a live failure pinned the card to Failed over other panes' live work. It now ranks below working, monitoring and permission, and above every finished outcome.
* refactor(orchestration): one owner answers a structured worker's custody
Routing, group addressing, worker-show and the idle sweep each composed their own reading of
whether orchestration still holds a structured worker, so each new obligation or retirement state
had to be added to every reader. structured-worker-custody now derives both answers from the
worker-terminal list state coordinators see in worker-list: addressable is owned and not released,
and owed work is an active custody or an unsettled task dispatched to the same incarnation. The
owner's state is read through the remote dispatch attachment too, as the terminal transfer lookup
already does. Behaviour is unchanged; a settled worker awaiting its coordinator still rests.
* refactor(orchestration): owed work is an open dispatch on the worker's incarnation
A supervised worker's own dispatch context stays open exactly while the worker is active, so the
separate active-custody branch only repeated it. Owed work is now one fact, which also states the
policy that a worker awaiting its coordinator's decision may rest, and both custody decisions are
written once at the top of the module.
* docs(agent-status): a departed agent's failure ranks below live work on the worktree card
* fix(native-chat): a restart offer knows its continuations by a tag in their id
The offer recorded each continuation id in a list on its capsule entry, capped at 16, and a running
action's id in memory. Both could disagree with the journal: past the cap an old rejected
continuation read as the chat moving on, and a crash during a retry restored the failure's older
entry, which lacked the retry's id. Each continuation id now carries a tag derived from the offer
(its teardown and chat), then the action's own part, so any continuation of this offer, queued or
rejected, is recognised from the journal row and the marker alone. The persisted list, its cap and
the in-memory action map are deleted; the agent-start withdrawal keeps an offer whose own
continuation the start was for, read against the stored marker.
* test(runtime): the legacy-worker reveal test judges its stale snapshot inside the wait
The tui-idle probe reads through readTerminal, which now awaits the structured
worker check before the PTY read, so the probe's snapshot request starts a
microtask later. vi.waitFor missed it on its first check and polled again at
50 ms, the same moment the wait's own 50 ms timeout fired. The stale snapshot
then resolved after the wait had already timed out, so the test passed without
judging it, and the rejection landed before any handler was attached. Vitest
reported that as an unhandled error and failed the shard.
Polling every 1 ms sees the request within a few ms, so the snapshot is judged
while the wait is still pending.
* fix(native-chat): a message held behind /compact is drawn where it was handed over
A message typed while /compact runs was drawn above the compaction's result, between
itself and its own answer. The reducer kept every item at the sequence and timestamp of
the row that created it, and a queued message is created at acceptance, long before the
command it waits behind writes its result. The phone orders by that sequence and the
desktop by that timestamp, so both put the message first.
A queued message now takes its position from its handover row, the same row that already
states its turn scope. Everything the agent did before the handover, a command it waited
behind included, draws above it. This holds for every held message, not only /compact's,
and needs no client change: every client, older builds included, reads the position the
host publishes. A live batch already carries the item when its dispatch row lands, and
history pages cut the reduced timeline by sequence, so paging stays contiguous.
* fix(native-chat): a phone's send during /compact answers without waiting out the compaction
A client that predates accepted-send replies, which is every phone build, has its send
reply held until the host hands the message over. A message sent during /compact is not
handed over until the compaction ends, so the phone's 15 s request timeout fired first
and showed the message as unconfirmed.
That wait now also ends once the message is queued behind a running command. This is
read from the journal's running turn and needs no new state. Every other wait still
ends at the handover: behind a starting child or an ordinary turn, and for restart
resume, the command front door and orchestration, which keep the plain handover point.
* perf(native-chat): a rewind places provider items with one pass over the merged rows
A Codex rewind gives each provider item the old epoch never held the turn record for its
provider turn. It found that record by scanning every merged row, restoring each row's
body, once per provider item. That is quadratic, and it runs on the host's main thread
up to the journal's 10,000-row cap, twice per rewind. A rewind record written before
rows carried their scope holds no scope for any provider item, so it paid the full cost.
The merge now indexes turn records by provider turn id once, keeping the first match as
the scan did, and each provider item looks its record up.
* fix(native-chat): a view never restarts a chat whose last start failed
A Claude chat whose CLI exits during startup left one red row per start, and
every time a view bound to it (the chat opening right after its create died,
or the user switching back to it) the hold started the CLI again, so the same
launch-failure row repeated. Only a send retries a failed start now, the same
rule provider-exit recovery already applied; the rule lives in one predicate
the hold, exit recovery and the delivery loop share.
* fix(native-chat): a message waiting behind /compact is drawn after it until it is sent
A message sent while /compact runs is placed where it was handed over. It was still
drawn where it was accepted until then. /compact writes its result one step before the
handover, so for that step the waiting message sat above the compaction's separator.
A message the host accepted but has not handed over is not part of the conversation
yet, so both clients now draw it after everything the agent has done. The shared
projection moves it to the end, which is the order the phone draws. The desktop ranks
it with the other not-yet-sent rows, after the streaming preview. At handover it takes
its place from its handover row, which is also after the separator, so it never
appears above the compaction it waited for.
* fix(native-chat): the idle sweep reads owed work every tick
Owed work counted as activity, but the sweep read it only once the idle window had elapsed, so it
refreshed the clock at most once a window. Work that ended just before the next read left the
agent to be stopped at that read, moments after the work ended, which is the gap the refresh was
meant to cover. The sweep now reads owed work on every tick for a started agent, so the window
always runs from the last tick that saw work owed.
* fix(native-chat): a continuation handed to the agent stays sent
The offer read its own continuation as not reaching the agent while its dispatch was pending, which
also covered one already handed over and still unanswered. When the wait for that answer ended first,
the failure it filed read as retryable, and a retry sent a second continuation to an agent that may
have acted on the first. Only a continuation still queued, or rejected, is now read as unsent.
* test(native-chat): start the child the loop waits on with an attach, not a second view
A view no longer starts a child whose last start failed, so the R2 case that
waits on a child started since the failure now gets that child from a client
attach, the one non-send starter left.
* fix(native-chat): settle a gone generation's turn wherever a conversation opens
A send that opens a chat this process had not read yet (after a crash, from a
phone or the CLI) went through the delivery open, which never settled what the
dead generation left running; only the read restore and a successful acquire
did. When the send's start then failed, the turn stayed running for every
reader. The settlement now runs in the one journal open, at the crash boundary,
for every opener except an acquisition, which settles from the evidence it read
before its reserve; the read restore's separate step is gone.
* test(native-chat): prove the next child's start settles the turn an earlier child left
The R1 case lost its only settlement assertion when the latch it checked was
deleted. It now seeds the running turn the earlier child left and asserts it
ends at the exit's receipt, with the exit's row, before the message is handed
to the new child.
* test(native-chat): count a failed start's rows by row, not by text
Comparing the set of texts passed when two different rows carried the same
words, which is the duplicate the test exists to catch.
* test(cross-version): load the phone row readers without mobile's toolchain
Vite transforms a file against its nearest tsconfig, and mobile/tsconfig.json
extends expo/tsconfig.base.json, which the root-only cross-version lane never
installs. The worktree ps verdict suite imported the current phone row reader
from mobile/ directly, so CI failed with TSConfckParseError before any test ran.
The harness now imports a copy of the working-tree reader placed under the
checkout cache, where the root tsconfig applies, as it already does for the
release checkout's copy. Both readers are still the real files.
* test(cross-version): keep the checkout path-guard message and justify the copy import's cast
* fix(native-chat): a command ends only by its own provider answer or its child's end
Stop no longer settles a conversation command. It interrupts it like any turn,
and when the provider cannot take that (Codex has not opened the command's turn
yet, or Claude refuses the interrupt) it stops the child, whose dead-generation
settlement writes the verdict.
The pending command now lives on the provider child's own session instead of an
adapter-wide map keyed by session, so it dies with the child and nothing has to
release it. Claude's /compact is sent under a uuid the slot records, and only a
root result naming that input (or naming none) ends it; its outcome is read with
the ordinary result reading, so a stopped /compact is a cancellation.
* fix(native-chat): a command's settle answers its message before ending its turn
The two writes are not one batch. Writing the message's answer first means a
crash between them leaves a running command turn, which the stale-turn sweep
already settles, instead of an ended turn whose message reads as in flight
forever. The settle now writes only while the command turn is still running.
* fix(native-chat): "Worked for" counts from the handover, not the send
A message held behind /compact, or behind a cold start, used to count the wait
as the agent's work, although its row is drawn at the handover. Every handed-over
submission's turn, the command's own included, now starts at the handover row's
instant, falling back to the send time for a host that recorded none.
* test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget
* test(native-chat): the interrupted create's own retry continues again
The merge of main's lease-latch fix replaced that test's retry of the interrupted create, under its
own operation id, with a fresh start whose result nothing read. That fresh start passes with the
released-reservation continuation deleted, so the case the fix exists for went untested. The retry
and its assertion are main's again.
* docs(native-chat): three comments that still had views starting agents
A start with nothing queued now comes from a command, goal change or rewind; an interrupted compaction
left alone would refuse every send, so no agent would ever start to finish it; and a current host
raises the unattached read refusal only once quit began, with the attach window belonging to an older
host.
* test(native-chat): pin the open's and the send's start and row counts, however the view binds
Opening a fresh chat whose starts fail makes one start and one row, with two
views bound before or after the create's child died; one send makes one more
of each.
* fix(native-chat): a second Stop on a command ends its child; one compaction verdict for every provider
A Stop's note now names itself in its key, so a later Stop on a command still
running reads, from the journal, that the provider was already asked and never
answered, and stops the child instead of interrupting again. Nothing is held in
memory for it.
Adds the rule both translators will read a compaction's end by: only a
compaction the provider reported is a success; none after Orca's interrupt is a
cancellation; anything else is a failure. A real Claude capture, pinned as a
fixture, is why: a stopped /compact ends in the same success result as a
finished one.
* test(native-chat): a reader's open settles the turn a failed exit settlement left running
An exit whose settlement write failed leaves its turn running in the open journal. PR 1's open now
settles it, and this pins the two reads that reach it here: a reader reopening a chat the idle
sweep closed, and a read that opens the chat before the restart restore reaches it.
* test(native-chat): the view-start test's starting window outlasts two subscriptions on a loaded runner
A subscription reads the conversation before it returns, so under load the two views took longer
than the create child's 300 ms start, which then exited before the test checked that it had not.
The child now takes a second to fail.
* fix(native-chat): settle a gone generation's turn at every open but an acquisition's
The journal open skipped the settlement whenever the lease read reserved or
live, to leave an acquisition's own open to the acquisition. But a lease a
crashed process left in recovery also reads live, until the next acquire
resolves it. A send that opened such a chat, from a phone or the CLI after a
crash on a host that could not prove the old owner gone, skipped the
settlement; when its start then failed, the dead turn stayed running for every
reader. The acquisition now says it is the opener, and every other open
settles, whatever the lease still claims.
* test(native-chat): hold the create's start open until the views bind
The "view binds while the create is still starting" case gave the create a
300 ms head start and asserted the views bound before it died. On a loaded
runner the holds took longer, the create's exit landed first, and the case
failed its own precondition. The create's initialize now waits on a gate the
test releases once the views are bound.
* refactor(native-chat): the provider's translator ends a command's turn; the loop holds no command state
A conversation command is now a turn of the provider child's own journal
pipeline. The adapter-wide tracker, its promise and the loop's settle step are
gone.
- Codex: the translator claims the provider turn that carries the command, scopes
its rows to the command's turn, and writes the command's end in the same batch
that settles that turn. Codex's own compaction marker is the success row.
- Claude: the command's turn is the translator's open turn until the result that
answers the /compact input ends it. The command's own frames, such as the
continuation summary, its echo and "Compaction canceled.", draw nothing.
- Both read the end with the one compaction rule: success needs the provider's
report of the compaction; none after Orca's interrupt is a cancellation.
- The message resolves at the provider's receipt, as any send does: the Codex
ack, or the Claude slash-command waiter on its result. The host writes a
command's end only when the provider never took it.
- The delivery loop stops while a command's turn runs, and every journal commit
re-wakes it through the session's serialize, so an end that lands while a step
decides to stop is never lost. A child that ends first is settled with it.
* test(native-chat): pin a command's end to real /compact frames and to each path it threads
The captured /compact frames drive the Claude translator's command turn: a
finished compaction ends as a success with only the separator drawn; a stopped
one ends as a cancellation with no failure row, and the next send answers in its
own turn; a result naming another input ends nothing. The command's end is
checked at each point the ordinary result path threads through: the reopen latch
after a failure, the settling of a child still working, the context facts the
result reports, and the provider's own error row.
On the host: a message held behind a command is handed over when the command
ends just as the loop stops for it, a refused command settles as a failure and
the loop moves on, and a Claude child that exits mid-command settles the command
and hands what waited to a fresh child.
* test(native-chat): tests merged from the base state which turn their rows belong to
* refactor(native-chat): drop the child-end waiter nothing waits on
A command no longer waits for its child here: its turn ends from the provider's frames or from
that child's settlement, and the delivery loop is woken by the commit. The waiter and its test
were left from the earlier shape.
* fix(native-chat): a command holds the queue only while its child runs it
The delivery loop stopped whenever the journal showed a command's turn running. When the
command's child ended and its settlement could not be written, that turn stayed running with
no child to end it, and the loop's gate kept it from ever starting the next child, which is
what settles a gone generation's leftovers. Every later send was held for good, and Stop had
no child to end.
The gate now holds only while the conversation has a child: with none, the command belongs to
a gone generation, and the loop's start settles it like any turn a dead child left running.
* fix(native-chat): a Claude /compact succeeds only on its compaction boundary
The command's evidence counted Claude's `compact_result: 'success'` status as the compaction
done. That status comes before the boundary that replaces the history, so a Stop landing
between the two read as a finished compaction even though no boundary was ever written. Only
the boundary now counts, as the rule for both providers states; the capture's finished
compaction carries one, so it still reads as a success.
* fix(native-chat): a Claude child's exit says why the turn it ended stopped
When a Claude child exited mid-/compact, the command showed "Worked for 0s" and no reason. The
child's translator ends its open turn the moment the exit is reported, stamped with the exit's
instant, so by the time the exit settlement ran nothing was running. The settlement recognises a
turn the exit already ended by that same instant, but the Claude lifecycle event dropped it on the
way to the host, which then used its own clock, matched nothing, and wrote no row. When the clocks
did agree, the row was scoped to the running turn, of which there was none, so it landed outside
the turn it explained.
The exit's instant now reaches the host, and the exit row belongs to the turn the exit ended:
still running, or ended by the translator at that instant.
* fix(native-chat): a message waiting behind /compact draws below its live activity
A message sent while /compact runs waits on the host until the command ends. Both clients moved
it to the end of the transcript rows, but the running turn's live activity line ("Compacting the
conversation") draws after every row, so the waiting message sat between the command and its own
live status.
A row that is queued, and not what the live turn is for, now draws after that live activity: on
desktop outside the transcript window, below the activity line; on the phone in the list footer,
below the live status. A message whose own start is pending still draws above the activity that
start reports.
* fix(native-chat): only a running command holds a message below its live activity
A message is accepted, then handed over a moment later, and in between it reads as waiting. Every
message waiting behind a live turn drew below that turn's activity line, so an ordinary message
sent while the agent was working crossed below "Thinking" and jumped back up once it was handed
over, on desktop and phone. Only a conversation command's turn holds the queue on the host.
A message now waits below the live activity only while the running turn is one a command opened,
read from the entry that opened it. The phone test also typechecks, which the mobile test ratchet
requires.
* test(codex): the claim test names its notification params as a record
* test(native-chat): a read that reaches a crashed chat before the startup reconcile settles its turn
On desktop the chat on screen at relaunch reads before startup reconciles the leases, while the
dead process's lease still reads live. The open settles the turn it left running anyway, and the
restore that follows finds it settled.
* refactor(native-chat): drop the composer's second error formatter
After the merge with main, every chat write in the composer path reports its
failure as a typed outcome worded by the refusal-notice table, so the send's
catch sees only a local throw. The {code, message} formatter this branch added
for it has no payload left to format, and its claim to be the one way a chat
words a failure is no longer true. The composer send is main's again.
* test(native-chat): pin the reason on a message rejected while its chat was closed
The reopen test checked only that the message reads as not sent; it now also
checks the Retry row carries the host's reason.
* docs(native-chat): drop the removed dispatch hold from six comments
A worker's session no longer takes a dispatch hold, and no release clock
rests a chat by visibility; the agent-launch comments, the abandon test,
the teardown test and the refusal census still said so.
* test(native-chat): rest the owner-status chat through the idle sweep, not a hold
The activation-gate test from #22808 put its chat at rest by holding and
releasing it, and passed the release-clock grace. This branch deleted both,
so the case threw before it reached its assertions. It now moves the host's
clock past the idle window and lets the sweep stop the agent and close the
conversation, then asserts the same owner answer and activation gate.
* fix(native-chat): show the structured pane's retrying line when a read fails
The read transport always hands the pane the host's words, so the error
state's "Orca keeps trying to load it" line, which showed only when there
were none, was never seen: the pane showed the host's text twice, as its
subtitle and on the status line under it. The structured pane now always
says its read keeps retrying, and the host's text stays on the status line.
The terminal-backed chat is unchanged.
* fix(native-chat): a send the provider never received after a restart has no verdict
Restart reconciliation rejects a crash-stranded send that is absent from a
trustworthy provider history with reason 'not_delivered'. Nobody failed that
send, but the verdict allowlist did not name it, so after a crash the chat
read Failed, was listed, and could notify "failed". Give the reason a shared
constant (persisted value unchanged), add it to the no-verdict set, and treat
it as an internal marker so the Retry row no longer shows the raw string.
* fix(native-chat): a failed Codex compaction's late completion writes no turn of its own
Codex ends a failed turn with an error and then still completes it as failed.
The error settled the compaction and released its claim on the provider turn,
so the completion read that turn as an ordinary one and wrote a stray record.
The claim now lasts until the completion, which adds nothing to a command the
error already ended.
* test(native-chat): the mid-command exit case resumes its next child as a real one does
The case's fake started every child as a newly created thread with the same generation. The
store refuses a created link once the conversation has a thread, so the next child's start
failed and wrote its own error row, which landed before or after the case read the journal.
The next child now resumes the thread under its own generation, and the case reads the
journal once the waiting message is delivered, which also proves the loop moved on.
* fix(native-chat): a /clear that never committed no longer locks the chat
A /clear wrote a durable "prepared, outcome unknown" record before starting
the replacement conversation. When that start was refused without a definite
answer (or Orca died), the record stayed forever, and while it did the chat
refused every send, /compact, a new /clear and rewind. Its only exit was a
rerun under the same operation id, which only the renderer held.
The record guarded nothing the process does not already know: a clear in
flight holds the session's serialize for its whole run and the command
controller refuses sends meanwhile, and the replacement's id and start
operation are pure functions of the clear's operation id. So the clear now
writes nothing durable before its commit, the gates refuse only a committed
clear (an older build's prepared record is inert), and a clear with no
committed answer reruns: a same-op retry re-attaches the same replacement,
a new op id runs a fresh clear.
A crash between the replacement's start and the commit leaves a replacement
record nothing points at. Verified: it has no tab, is not in the
replacement list, and a restart opens and starts nothing for it (restore
reads only the visible tab index); restart reconciliation releases its lease
like any dead owner's. In a live process its agent is stopped by the idle
sweep like any quiet agent. Session History lists provider transcripts and
only annotates them with an owner, so it can list this only if the provider
wrote a transcript for a thread that never got a message. Its record stays
on disk, as every closed chat's does; the store deletes none.
* fix(native-chat): a Codex rewind the provider did not keep no longer fails every attach
When Codex acknowledged a revert and Orca stopped before proving it, the
rewind stayed prepared with providerApplied set. On the next attach,
recovery read the provider's history, found the target turn still there
(provider-refused), and threw, because that settlement was limited to
reverts never sent. The throw ran inside the attach, so every attach, and
every send that needs one, failed for good.
The journal is replaced only once the provider proves the revert, so both
the provider and the journal still hold the target turn: settling the
rewind refused is consistent whether or not the provider acknowledged it.
* test(native-chat): a clear retried after a crash starts no second replacement
The replacement's id is the only thing that keeps a retried clear from leaving a second one, and no test held it across a restart.
* chore(native-chat): the clear rerun comment claims only the stable replacement id
* test(native-chat): wait for a send's background start before the refusal oracle removes its store
An accepted send wakes the delivery loop, which starts the agent in the background. The oracle's teardown disposed the loop but did not wait for that start, so its lease write could create a temp file in the store directory while the directory was being removed, failing the test with ENOTEMPTY about one run in four. The teardown now drains tracked starts before it closes the journals.
* fix(native-chat): a start a message waited on gets one failure row, the delivery loop's
When a queued message's start failed, two writers could report it under the same row: the delivery loop, when the adapter settled the start without proving it, and the exit settlement, when the child's exit landed. The last one won, so the chat's row could name a different cause than the one the message was rejected with, or be written twice.
The exit settlement now writes the start's row only when no message is queued and the loop has not already recorded that start. A start for a command, goal change or rewind, with nothing queued, still gets its row from the exit.
* fix(native-chat): a /compact whose start failed says to run /compact again
The failure-words context named only /clear as a command to retry, so a
/compact whose agent failed to start read "Send your message to try again."
on its row, its rejected message and the command reply. The context now
carries any conversation command; the host derives it from the oldest
message still waiting on the provider, which is the one a failed start
fails first, and the /compact reply names it directly.
* fix(native-chat): a Codex /compact ends only on its turn's completion, below Codex's own error row
Since only turn/completed ends a Codex turn, Codex's turn-ending `error` is a row
inside the still-open command turn, and the failed completion that follows it is
the command's end: completed, outcome failure, at the completion's receipt time.
The command's own "Compaction failed" row was written on that completion too, so a
failed /compact read its reason twice.
The command turn now notes when Codex's turn-ending error for the turn it carries
was written as a row, and its end then adds no second row. A retried stream error
ends nothing and is not counted. The flag that let the error end the command and
kept the claim until the completion is gone with the error-driven end.
A test replays the captured failed compaction from the real app-server through a
claimed command turn.
* test(native-chat): main's crash-turn test states its row's turn, and a dead /compact settles on its recorded exit
Two tests the main merge brought together:
- The crash-turn test from #23456 writes a turn record through the event sink
without options; every row here states its turn scope, and a turn record's is
the thread.
- The /compact whose exit settlement could not be written no longer stays running
until the next start: main now settles an open chat from the exit it recorded, so
the command reads interrupted before the next message, which is then delivered.
* test(native-chat): main's new journal tests state each row's turn
The crash-turn, stale-turn and sink-queue tests main added wrote rows without a
turn scope, which every item write now states. Rows written inside a running
turn name that turn; the sink-queue batch and a send handed over with no live
turn name the thread.
* fix(native-chat): draw a queued turn's message after the earlier turn's rows
A message sent while A runs is written to the journal when it is sent.
When the provider queues it (Claude answers it after A), A's remaining
rows - its last tool run and its answer - are written after that
message, and the message's own turn opens only after them. Grouping put
those rows in A's turn, but the transcript still drew them in journal
order, below B's bubble and bar, where A's answer read as B's reply. This
is the residual #23671 left open.
A message that opened a turn now draws after the earlier turns' rows the
journal wrote after it, just before its own turn's rows
(nativeChatTurnDrawOrder, returned by nativeChatTurnMembership as
drawOrder). Desktop and mobile both draw in that order. A steer, and a
message that has opened no turn yet, stay where they were written. It
applies on hosts that state turn scopes and, through journal order, on
older ones.
* test(native-chat): run #23026's Stop tests against #23059's command turns
Two of #23026's tests call APIs #23059 changed, and failed after the
merge:
- codex-structured-conversation-stop: a compaction now goes through
adapter.compact with the command run the host wrote (#23059), not a
bare turn id, and answers with the provider's receipt. With the command
claimed, a Stop that names no turn while the compaction's provider turn
has not opened still interrupts nothing.
- main-agent-working-agreement: a provider row states its turn scope
(#23059's appendItem contract); the retry and subagent rows are
conversation-scoped.
* fix(native-chat): typecheck main's Stop and restore-grouping code against #23059
A Stop's compaction interrupt reads the narrowed requested turn, and the
restore-grouping test states whether each row reports its turn's outcome.
* fix(native-chat): say a /clear cut off by a restart left the chat unchanged
A /clear retried under the same operation after Orca restarted could not reuse the new conversation its first try started, and its row said "Codex couldn't start. Run /clear again." The agent did not fail to start: the earlier try was cut off. The row now reads "This /clear didn't finish, so the chat is unchanged. Run /clear again to start fresh.", from a new clearUnfinished failure fact written through agentSessionFailureWords.
The clearUnconfirmed and conversationCommandUnconfirmed reasons stay, with their words, for older hosts that still send them.
* fix(native-chat): a retried /clear finishes onto the conversation its earlier try started
When an earlier try of the same /clear started its replacement conversation and a restart or the
idle sweep has since stopped it, the retry could not replay that settled start and reported the
chat unchanged. That replacement is a fresh conversation at rest, so the retry now commits onto it
and its first message starts its agent. A replacement whose start definitely failed still reads
that failure, and one Orca can't prove stopped still commits nothing. The clearUnfinished failure
kind this made unnecessary is removed.
* refactor(native-chat): stop recording that Codex acknowledged a rewind
A refused rewind recovery now settles as refused whether or not Codex acknowledged the revert,
so nothing reads providerApplied any more. Stop writing it and drop the hook that wrote it.
Records that still carry the field load as before; the schema ignores the extra key.
* fix(native-chat): a /clear retried under a new operation id finishes the same replacement
A /clear's replacement id came from the client's operation id, so a retry the client sent
under a fresh id started a second replacement and orphaned the first. The host now derives
it from this caller's oldest /clear since its last commit whose replacement start reached
the operation ledger, so any retry from that caller finishes the same replacement, including
after a restart. A /clear after a committed one starts a new replacement. Another caller's
/clear is refused only while such a replacement is running or not proven stopped. An older
client that resends the same operation id still lands on the same replacement.
* fix(native-chat): a /clear retry never repeats a failed start or waits on an unproven stop
A retry under a new operation id could pick an earlier try whose replacement start had already
failed, replay that failure and commit it again, so a user who had since signed in was told
they were still signed out. Such a try is now skipped, and the retry starts afresh.
Another window's /clear was refused while the first window's leftover replacement was merely
not proven stopped. Nothing but the first window's own retry would settle that, so the refusal
could last until its ledger row expired a day later. It now waits only on a replacement whose
agent is running.
* fix(native-chat): a /clear retry finishes only a replacement that started
A retry picked an earlier try whose replacement start never answered, because a crash left
that start unsettled. Replaying it could only repeat "couldn't start" or, with the old agent
unproven, refuse every /clear from that window. Only a start that succeeded left a
conversation to finish; any other try is skipped and the retry starts afresh.
* refactor(native-chat): a record's identity fields are built in one place
A created record and a founded one (a conversation no agent has run yet, at
rest) share who and where the agent is and how it launches. The founding
builder is used by the /clear commit that follows.
* feat(native-chat): the store commits a /clear and its new conversation in one write
commitConversationClear founds the at-rest replacement from the cleared
record's identity and writes the committed marker and tab move in the same
transaction, so neither can land without the other. It refuses to overwrite
an existing record under the replacement id.
* fix(native-chat): /clear starts nothing; the new chat's first message starts its agent
/clear used to start the new conversation's agent before it committed, so it
could fail on that start ("Run /clear again"), and a crash between the start
and the commit left a running conversation nothing pointed at. #23524 then
needed a ledger scan to find an earlier try's replacement, a nonce half of the
derived ids, a refusal of another window's /clear while a leftover agent ran,
and a check for a start that had already finished.
Now /clear opens the chat for writing (it no longer starts an at-rest chat's
agent either) and makes one store write: the at-rest replacement under a
random id, the committed marker, and the tab move. The first message in the
new chat starts its agent through the existing send and delivery path, fresh
because its handle chain is empty. A failed start shows on that message with
the typed failure and a Retry, and a conversation no agent ever ran now reads
"couldn't start" rather than "couldn't restart".
Deletes clearTryToFinish, otherCallersClearIsLive, the attach block and the
committed start-failure branch, and the tests of that retry machinery.
* test(native-chat): drop the /clear retry wording test; no start runs for a /clear now
* test(native-chat): another window and a phone read a /clear's replacement from the host
Both list the replacement the committed marker names, under the chat's tab,
and each one's session list shows it with nothing unread until its first
message runs. A reader that recomputed the id from the operation turns this
red.
* test(native-chat): a never-started replacement closes as settled
A worktree delete closes every chat in it and asks the user to force any it
cannot prove stopped. A replacement no agent has run is released, so its
close settles like any at-rest chat's.
* fix(native-chat): /clear settles an interrupted Codex rewind the way a send does
/clear moved from starting the chat's agent to only opening the
conversation. A Codex rewind cut off mid-way on a chat at rest can only be
settled by its agent, so /clear was refused as "rewind unconfirmed" every
time until the user happened to send a message. It now prepares like a send
or /compact: the agent starts only when such a rewind is in doubt.
* fix(native-chat): a chat whose agent is not running keeps its `/` commands
Claude reports its skills and project commands only from a running process,
and the host served the `/` menu only from the running agent. Now that
/clear starts nothing, the new chat's menu lost those entries until its
first message; a chat stopped by the idle sweep already did.
The host now keeps, in memory, the list a running agent last reported for
its launch (provider, host, workspace, account and launch arguments) and
serves it to a chat of the same launch whose agent is not running. A new
report replaces it; nothing is stored on disk, so a relaunch still shows
the short menu until the agent reports again, and no list is ever served
across accounts, workspaces or hosts.
* test(native-chat): queued drafts around /clear follow what a /clear now is
Three queue tests from #23726 are red on main
|
||
|
|
449b8ca17d |
fix(drop): route local terminal and composer drops through the resolver (#24009)
* fix(drop): copy macOS drag-temp files so the PTY daemon can read them
macOS screenshot thumbnails live in $TMPDIR/TemporaryItems/NSIRD_*, which
only processes attributed to Orca main may open. The detached PTY daemon is
not, so agents in local terminals get EPERM and Claude Code silently drops
the paste.
fs:resolveDroppedPathsForAgent now copies those files, and only those, into a
private per-user orca-drops-<uid>/orca-drop-XXXXXX/ directory, keeping the
original name. It streams from an O_NOFOLLOW handle capped at the inspected
size, so no xattrs (com.apple.macl) come along. The copy is 0600 in a 0700
directory, bounded by the remote-import per-file and per-drop limits, and
rechecked for changes. Other paths pass through. The local branch returns
per-item results, authorizes what it returns, and accepts no worktreePath.
Expired copies are swept 7 days later.
No renderer calls the local branch outside WSL yet, so this ships dark
until the renderer routes local drops through it.
* fix(drop): route local terminal and composer drops through the resolver
Local terminal drops pasted the dropped path directly, and composer drops
attached it after a per-path authorize call. So a macOS screenshot thumbnail
reached Claude Code as a path in a folder the PTY daemon can't open, and the
paste was silently dropped.
Every local terminal drop now calls fs:resolveDroppedPathsForAgent, pastes
what it returns, and reports skips and failures with local wording ("Could
not prepare N dropped files"). The WSL-only branch and the direct-paste
branch are gone; the local-WSL path mapping stays as a step after
resolution. The composer resolves the whole drop in one call, without a
project path so its attachments never get the WSL rewrite, stats only the
resolved paths, and folds resolver skips and failures into its existing
toast. The pane, transport, mounted and owner checks still run after the
await.
* test: verify resolver and write errors surface independently on drop
Add a test case ensuring that when path resolution and PTY write both fail during a file drop, the UI reports both failures separately rather than letting the write error mask the resolution issues. Refactor error handling in pasteLocalDropPaths to catch IPC resolution errors immediately, then handle paste errors separately, so skipped/failed files are always reported via the finally block regardless of outcome.
* test: extract transport variable in drop resolution test
Improves readability by extracting the terminal transport creation from the Map initialization.
* refactor(drop): extract native file drop relay and temp staging utilitie
- Move the native file drop relay queue from attach-main-window-services into
a dedicated native-file-drop-relay module so it owns the async copy and
forward pipeline for drag-temp files, separate from main window setup.
- Extract shared temp-directory management (ownership checks, sweeps,
permissions) into owned-temp-staging-root, used by both drag-temp copies
and remote clipboard staging.
- Simplify dropped-path-resolution to handle only the WSL path rewrite on
local worktrees; the relay handles macOS drag-temp copying before it
reaches terminal/composer drop handlers.
* fix(drop): pass drag-temp files through uncopied with timeout and budget
Large files exceeding the copy budget are now passed through uncopied instead
of rejecting the drop, so copy failures don't lose the entire interaction.
Copy timeout prevents hung copies from blocking subsequent drops, and budget
tracking accounts for retained copies to prevent disk fill.
Extract darwin-user-temp-dir to resolve the correct macOS per-user temp dir
rather than relying on $TMPDIR.
* fix(drop): discard queued drops on renderer reload
Capture the renderer's lifetime when a drop is enqueued. When the
renderer reloads before a copy completes, the operation cancels and
queued drops are discarded, preventing stale content from reaching
the reloaded document.
* fix(drop): serialize drag-temp copies and localize failure reasons
Main no longer sends user-facing failure messages; instead it sends reason tokens
that the renderer localizes. Drag-temp copies run serially under one byte budget
with a pending-copy limit, so non-temp drops can overtake. When a copy stage
aborts, remove any partial copies made so far. Distinguish 'uncopied' (original
handed over) from 'failed' (couldn't get it at all), and add specific reasons for
storage, permission, timeout, and budget exhaustion.
* fix(drop): serialize drops and extend TTL to 7 days
Ensure drops reach the renderer in arrival order by queuing all path drops,
not just copies. Extend TTL from 24h to 7d to support lazy readers like
drafts and startup prompts. Withhold uncopied files from agents that can't
open originals (terminal, composer), keeping editor-only access working.
|
||
|
|
4e8edc8872 |
feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)
* feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) Every operation SshConnection admits (exec, shell, sftp, file transfers, upload sessions, forwarded channels and sockets, system-SSH commands) now runs through the connection's work ledger, and every ssh2 client and proxy process it allocates is tracked until it physically closes. Ordinary connect, reconnect and disconnect behavior is unchanged. Adds: - subscribeTransportClosure: one-shot notice once the connection is disposed, every allocated transport has emitted 'close' and tracked work has drained. System-SSH startup is never proven closed from here. - disconnectAndDrain(signal): for owned single-lifetime transports; fences new work, disconnects, and waits for physical close of the client, proxy, every allocated client and all fenced work. Refuses (after cleaning up) when the transport cannot be proven, e.g. system SSH or a connect still in flight. - getExecutionDestination: the ssh2 endpoint, accepted host-key fingerprint and proxy-route digest proven by the current handshake (ssh-connection-destination). - getTransportGeneration, prepareForwardRoute, openForwardSocket, forwardOut, forwardStreamLocal for later forwarding callers. - An automaticReconnect constructor option (default on). Channel close is local lifetime evidence only, never a remote-exit verdict. Porting note (source: #16741 head |
||
|
|
99e0303572 |
fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)
Paired clients read RuntimeMobileSessionTab.incarnationId to fence remote
identity observations (#18078), but projectRuntimeMobileSessionTabs never set
it, so remote-runtime-pty-transport always saw null and the fence never
engaged. The projection now publishes the incarnation of the PTY record that
owns the issued handle, never a stale incarnation persisted on the tab.
Ported from #16741 (
|
||
|
|
60c93263cc |
feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)
* feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7)
Moving a project to another profile now carries its hidden-frontmatter
overrides, rekeyed to the destination worktree's editor file ids, and
removing a repo prunes the overrides that belonged to its open files.
The owner-keyed session projection moves into profile-session-owner-transfer
(extractSessionOwnersForTransfer, exported for later reuse), and
extractSessionForTransfer becomes a thin repo-rekey projection over it. Terminal
layouts of unified-only terminal tabs now travel with their tab.
Ported by hunk from #16741 (
|
||
|
|
817af768b0 |
feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)
Each data event the terminal daemon writes now carries an optional
payload.incarnationId naming the PTY incarnation that produced the bytes, so a
later consumer can tell output of a respawned session from its predecessor.
- Session hands its incarnation to the output plane; identity-aware stream
clients receive it through onDataWithIncarnation, legacy clients keep the
old callback shape.
- The stream batcher never merges queued output across incarnations and
budgets the field when splitting oversized lines.
- Optional NDJSON field only: no new event type, no protocol version change
(check:daemon-protocol-crossing is unchanged), and older readers ignore it.
Ported by hunk from #16741 (
|
||
|
|
c9918931c8 |
feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)
* feat(ssh): add the SSH connection work ledger and channel lifetime tracking Dormant foundation for the T2 SSH core port of #16741 (design D9.3). No production module imports it yet; only tests do, so SSH behavior is unchanged. The next T2 slice wires SshConnection.exec/sftp/channel opens and forwarded sockets through it. Adds: - SshConnectionWorkLedger: tracks local operation and channel lifetimes per connection. fenceForReset closes admission (except work nested under an already-admitted operation and one exactly-identified control channel) and drain() waits for the rest, failing sticky on any failure or unverifiable opening observed after the fence. Channel closure is local lifetime evidence only, never proof that a remote process exited. - trackSshConnectionChannelLifetime / openTrackedSshSocket: bind an opened channel or socket to its ledger entry and settle it only on 'close' ('end', 'error' and the destroyed flag are not closure). Porting note (source: #16741 head |
||
|
|
dc08ffeba9 |
feat(relay): fence and drain file and git response streams on shutdown (#24185)
* feat(relay): fence and drain file and git response streams on shutdown Third T1 slice of the #16741 port: the relay's two detached stream producers can now be fenced and awaited, not just flagged. RelayStreamRegistry (fs.readFileStream): - disposeAll permanently fences register/beginOperation (relay_file_stream_shutdown_fenced), waits for every close and for every admitted operation, and rejects with relay_file_stream_shutdown_incomplete while any handle is still unclosed, so a later disposeAll retries only the failed handles. - release coalesces concurrent callers onto one close, tolerates only EBADF as already-closed, and keeps a failed-close entry (aborted) for retry. - beginOperation / releaseUnregisteredHandle let work that opens a handle before registration be drained too (wired in the next commit). GitResponseStreamRegistry: disposeAll fences startStream (relay_response_stream_shutdown_fenced); disposeAllAndWait also awaits every scheduled or in-flight pump, and an aborted pump no longer publishes git.responseEnd after its final chunk write settles. reserveTerminalFrameSlot moves unchanged into fs-stream-terminal-frame-slots and now holds a registry operation per slot, so shutdown also waits for undelivered fs.streamEnd/fs.streamError frames. No wire change: no new methods, fields, or opcodes. Mixed versions are unaffected; a fenced relay answers with an ordinary request error. Porting note (source: #16741 head |
||
|
|
a789233bbb |
refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)
* feat(runtime-rpc): Node WebSocket lifecycle extraction and a required-port bind mode Port the #16741 T1 transport pieces onto the current Node-only transport. The per-socket lifecycle (pre-auth timer, heartbeat enrolment, finalize on close/error), over-capacity rejection and shutdown move out of ws-transport.ts into node-websocket-lifecycle.ts, and the size/connection caps into websocket-transport-limits.ts. WebSocketTransport gains an opt-in strictPort that binds only the configured port and fails closed instead of walking the fallback ladder; OrcaRuntimeRpcServer exposes it as requirePinnedWsPort, which also skips the persisted fallback port and stops the Unix socket transport before rethrowing, so a caller whose tunnel forwards one fixed port never runs on a port nobody can reach. Porting note (#16741 |
||
|
|
0b812bd698 |
feat(relay): route relay handlers through work admission; producer publication drain (#24181)
* feat(relay): route relay handlers through RelayWorkAdmission First consumer of the T1 work-drain seam: every request and notification handler registered on the relay dispatcher now runs under RelayWorkAdmission, and the dispatcher exposes beginWorkDrain(exclude?) and assertActiveWorkContext(context). RequestContext gains an optional transportGeneration, stamped from client.generation for both requests and notifications, so later producers can pin publication to one transport. Behavior today is unchanged: nothing in production calls beginWorkDrain yet (the shutdown/reset caller is T3), so admission never closes. Decision: pty.cancelDelivery joins the drain request allowlist. #16741 omits it. It only retires an existing source delivery (closes the ledger record, releases retained spans, wakes the publication so it drops its record), it cannot open a delivery or produce output, and a retry replays the remembered proof instead of mutating again. It is the request-shaped sibling of pty.ackData / pty.setDeliveryPaused, which are already allowlisted. Refusing it during a drain would make the client's cancellation proof fail (ssh_source_cancellation_proof_invalid) and leave the delivery retained until detach, turning a provable cancel into an unverifiable one. Revisit only if live ownership transfer (T7, deferred) needs a frozen source ledger. Porting note (source: #16741 head |
||
|
|
3aa2d3af7c |
feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)
Dormant seam for the Phase 3 port of #16741 (design D9.3): the shared
contracts later T1/T2 slices build on. No production module imports any of
it yet; only tests do. Existing behavior is unchanged: the call-queue hold
check is a no-op until something calls holdIdleSelectors.
Adds:
- RelayWorkAdmission + relay-work-drain-contract: closes relay handler
admission and waits for in-flight work, still admitting the cleanup
requests/notifications (cancel, acks, unwatch) that let that work finish.
- TransportPublicationDrain: counts local write completions against one
transport and fails sticky once that transport is replaced.
- pty-ownership-transfer-release-gate: exact-match opt-in
(ORCA_ENABLE_PTY_OWNERSHIP_TRANSFER_MUTATION=1) that SSH core uses to keep
live transfer off (D9.2).
- RuntimeRpcCallQueuePool.holdIdleSelectors + RuntimeRpcCallQueueBusyError:
atomically fence idle selectors while routing changes.
Porting note (source: #16741 head
|