mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 16:02:03 +00:00
ff59e2cd7b0f2e73d558bbffdd888d07aa83a437
9421
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ff59e2cd7b |
fix(codex): log a late reply to a timed-out request instead of showing it in the chat (#23893)
* fix(codex): log a late reply to a timed-out request instead of showing it in the chat * fix(codex): say a reply had no waiting request rather than an unknown id |
||
|
|
18327d9665 |
fix(claude): a queued message Claude withdrew is settled from Claude's own cancelled event (#23862)
* fix(claude): settle a queued send the CLI withdrew from its own cancelled frame Claude reports each uuid-stamped command's lifecycle (queued, started, completed, cancelled). A send it withdraws from its queue gets `cancelled` before the interrupt or cancel_async_message answer, so a lost or failed answer no longer leaves that send pending: it settles as withdrawn, with the same reason and words as the receipt path. A command the CLI already started also ends `cancelled` when its turn is interrupted or fails, so `cancelled` after `started` is not a withdrawal; an echoed send has left the waiter lists and is never reached. Tests replay real 2.1.280 captures, scrubbed. * fix(claude): release a doubted send when the CLI reports its session idle A Claude send whose write ended in doubt is recorded `unknown`, and a live `unknown` reads as work still owed, so the chat showed Working until the child exited. Claude sends `session_state_changed idle` only once its whole queue has drained, so it can no longer be holding that send. The runtime now routes that report to the host's existing release, the same one Codex's thread-stopped report uses; it retires `unknown` only, never `pending`. * fix(claude): keep a command's started mark when a redelivery re-emits queued; fixtures name msg_lifecycle_v1 |
||
|
|
d60f999f94 |
fix(native-chat): turn facts come from the turn record, and /compact is a message the chat sends (#23059)
* refactor(native-chat): remove the unused terminal handoff No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths. * fix(native-chat): never let the pre-stop snapshot hold a chat's stop Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop helpers only the terminal handoff called `claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and `queryWindowsProcessRowsFresh` lost their last caller with the handoff. The fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`, the teardown path that still depends on that contract. Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): stop citing the removed handoff in lifecycle comments Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stalled snapshot drain without a cast Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a start dead before proving owes no settlement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(terminal): remove the agent-session PTY write gate The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop the transcript helpers only the handoff called appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop calling a starting chat "mid-handoff" A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stand-in roster decoder without a cast Co-Authored-By: Claude <noreply@anthropic.com> * refactor(codex): name the pinned rollout lookup for what it does With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow. * refactor(native-chat): type the owner-status reply as the host sends it The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged. * refactor(native-chat): normalize terminal-handoff lease values once at decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind. * refactor(native-chat): stop threading the owner kind through a reservation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it. * test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write. * fix(native-chat): name a chat write by its target, not the owner generation A write carried the fence of the last frame the pane read, and the host refused it unless that fence was still current. An idle release and the restart after it each move the fence, and the release publishes nothing, so a send after a release was refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a cold start was refused as stale. Every write already names what it acts on: a send its conversation, a cancel its turn, a prompt answer its item revision, a rewind its epoch; an option is last-writer-wins. So admission stops comparing the client's fence, and the rebase that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it. The writer-lease check stays, and so does the attach's compare-and-swap. Frames now stamp the fence read when each frame is sent instead of a copy each subscriber kept, which went stale on the same release. * fix(native-chat): every journal append reaches the chats that are open A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted. * test(native-chat): an epoch replacement reaches the open chat * test(native-chat): each row reaches an open chat once, and a live handle enters only through the map * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. * perf(native-chat): a publish behind a delivered commit reads nothing Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read. * test(native-chat): state why the teardown test's fake journal is safe to cast * docs(native-chat): say mutation admission checks only the writer lease * docs(native-chat): drop the send rebase from comments that still described it * fix(native-chat): a message is accepted, then delivered A send to a chat with no running agent restarted the agent inside the send call, before the message was recorded, so the client waited for the whole start and a failed restart refused the message. Claude held prompts sent during startup, and those could settle as "unconfirmed". A send is now accepted inside the session's serialized queue: one ledger row and one submission row marked handoverRecorded, published, answered pending. A per-session delivery loop exists while a message is queued. It starts the agent through the same serialized attach a hold uses, waits outside the queue for a Claude child to prove its start, and hands the oldest queued message over as its own serialized step, writing dispatch{pending} before the adapter call. A start it needed and did not get writes one error-tone row and rejects every queued message with the same words; a start Stop cancelled writes none. Settlement follows from the rows. A queued message is provably unwritten, so a close, an eviction or an exit rejects it. A handed-over message stays in doubt. A queued row at or below the sequence a handle found when it opened was left by an earlier process and is rejected at open, with no latch. Stop withdraws queued messages with no writer lease and no fence. An attach failure keeps the conversation open, and the attach adopts its journal. Owed work counts the loop and queued rows. A compaction or rewind found prepared when a conversation opens was started under a child this process no longer has, so the open settles it rather than leaving it to refuse every send until a view attaches. The open cursor is scoped to its epoch, because sequences restart when an epoch is replaced. Deleted: restart-before-admission, recordFailedRestart, the fence rebase, Claude's startup gate, the attach's forget on failure and its own crash boundary. Clients without agent-session.accepted-send.v1 get their reply held until the handover; the desktop and paired desktop lists advertise it. * fix(native-chat): settle queued messages only for the child that ended A child that proved its start and then exited before its message was handed over left the message queued: the exit settlement returned early when nothing else was in flight. Delivery then started another child for it, and a child that died the same way started another, without end and without a row. A retried settlement for an earlier generation, run by the attach that delivery started, did the opposite: with that generation's turn unfinished it rejected the message queued for the child being attached. The settlement now takes the rejection for queued messages from its caller. The unexpected exit and the eviction pass one, and it applies even with no other work in flight; the retry for an earlier generation passes none. * fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. * perf(native-chat): the recovering open reads the journal once Every conversation open now goes through the recovering open, including the read restore of every chat at startup, which used to replay its journal once. The recovering open replayed it twice: once to probe it and again inside the open. The probe is now handed to the open as its load. * fix(native-chat): an attach that fails after indexing its child leaves no child behind A failed attach now keeps the conversation open, but a failure after `onAttached` indexed the child (the rewind or compaction recovery, or the attach's own success record) left that entry claiming a child the failure path had already released. The next send found the phantom, skipped the start, and wrote at a fence the journal had moved past, so the message stayed queued for good. The entry now drops the released child and its event sink, and follows the record's fence, as a failure before indexing already did. * fix(native-chat): a withdrawn message shows no error, and a rejection outlasts the send's answer The error strip for a message the host accepted and then did not deliver matched the entry before the outbox reconciled, so a Stop's withdrawal, which the reconcile drops, showed "Orca could not send your message" with nothing to retry. It now reads the reconciled entry. A rejection the journal records before the send's own pending answer lands is final as well: that answer no longer puts the entry back to dispatching with no Retry. * fix(orchestration): a structured worker whose agent outlasts the preamble wait is left unknown, not torn down The preamble waits for its submission to be delivered while the worker's agent starts. When that wait ran out it threw operation_unknown, and the failed-start teardown then closed the session, which rejected the very preamble the host was about to deliver. It now reports a turn start nobody observed yet: the worker is start-unknown with its session kept, the host delivers the preamble when the agent starts, and the worker's report settles the dispatch as for any unobserved start. The receipt no longer suggests reading a screen a structured worker lacks. * fix(native-chat): a message rejected while its chat was closed reads as not sent A remount reads an entry it left dispatching as unconfirmed. When the journal had rejected it meanwhile, as a failed start or a quit now does, the reconcile left it unconfirmed: it blocked every later message behind a Retry and no reason, and the delivery probe, seeing the journal already answered, never ran. The reconcile now settles it as rejected like a dispatching one. * test(orchestration): name why the readiness settlement fakes are cast * fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent * docs(native-chat): drop the fence from the admission the send effects run behind * docs(native-chat): give the fence move on release the reason that still holds * docs(native-chat): stop citing a write fence check in launch and mailbox comments Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease. * refactor(native-chat): the provider child is its own record A conversation now outlives any number of provider children, so the child is one record on the conversation's entry instead of five loose fields beside its journal. It is written in one place: indexed only once an attach has fully succeeded, and ended through one function that an exit, a failed re-attach, a Stop and an eviction all share, matched on the child's generation and fence. - A failed attach writes no child, so there is nothing to unwind: the field unwind and the fence patch after it are gone. - Conversation writes read the record's fence, the way mutation admission already does; a child's own writes use its fence. The four stored-fence patches, and the settlement retry's overwrite of the conversation's fence, are gone. - The owed wind-down is its own tombstone, carrying the child it is owed for, and is no longer dropped when an attach replaced the whole entry. - Stop on a child still proving its start stops only the child: its lease goes back and the chat is told it is idle, but the journal, the holders and the readers stay. Close is that stop plus the conversation's close. - The settlement retry uses the conversation's own journal, opened through the host's one open. * fix(native-chat): the delivery loop alone settles a message its start or child failed A queued message was settled by whichever path happened to end the child first: the loop, the unexpected exit, eviction's work settlement, the open's leftover rule, and the startup branch that rejected every pending row. That gave two failure rows with different tones for one start, a loop that could hand over to a different child than the one it waited on, and a Claude start that died while starting reading unlike every other failed start. - The loop remembers the child it waited on. At handover, if that child is gone or replaced, it reads how it ended: a Stop continues; anything else writes one failure row and rejects every queued message with the same words, then stops. A child still starting whose start the adapter says did not land fails the same way. The exit, eviction and the settlement retry only settle the handed-over and legacy rows of the child that ended. - One failure row, always an error, keyed by the start. A start a view began that dies with nothing queued writes the same row through the same builder, so a second report revises it. - The open no longer rejects leftovers; the loop's first step does, and the open wakes it. - `awaitStarted` answers why a start did not land, so the row says it even when the loop sees the failure before the exit is processed. - Quit closes every conversation the way closing a chat does: what is still queued is rejected as closed, with or without a child, and a start the loop already has in flight is waited for so the child it produces is stopped rather than left behind. * 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. * 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. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f442b165cb |
fix(editor): follow system light and dark while the editor tab stays open (#23848)
* fix(editor): follow system light and dark while the editor tab stays open The editor sampled the system color scheme once, when the tab opened. The rest of the window already listens for appearance changes, so the chrome updated and the editor surface did not until the tab was closed and reopened. Editor surfaces now use useDocumentDarkTheme, which re-renders on that change. An explicit light or dark setting still stays put. * refactor(theme): share one live dark-theme hook across every renderer surface Move useDocumentDarkTheme out of components/editor into hooks/ so non-editor code stops reaching into the editor folder for it. Convert the four surfaces that still read the system color scheme once at render (comment mermaid, both PR combined-diff viewers, automation prompt editor) so they follow OS light and dark changes like the editor now does. Replace the 145-line MonacoEditor mock test with a direct hook test covering system flips, explicit light/dark staying put, and unloaded settings. * test(theme): keep mounted-editor coverage and cover a converted surface Restore the MonacoEditor system-theme test so a mounted editor is still guarded against ceasing to use the hook, and add one for CommentMermaidBlock as a representative newly converted surface. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
b18434b5f5 |
feat(explorer): scope file trees to sparse checkout directories (#18750)
Co-authored-by: Neil <neil@stably.ai> |
||
|
|
62e3a5a9a2 |
fix(codex): turning Codex off per agent keeps its hook entry off (#23667)
* refactor(agent-hooks): one predicate for whether an agent's status hooks are on "Global switch on and this agent not turned off" was spelled out separately in the startup controls, the settings reconcile, the retained-home reconcile, the WSL preflight RPC, the CLI preflight and the OpenCode plugin selection. They now share one function, in a module light enough for the CLI's per-launch Codex preflight to load. The PTY spawn env derives the Codex flag from the switch and opt-out list it already carries, the same way it does for OpenCode and Pi, instead of receiving a second copy. * fix(codex): launch and resume prep honour Codex's per-agent hook opt-out Turning Codex off in the per-agent hook settings removes Orca's Codex hook entry, but launch prep and session resume read only the global hooks switch, so the next Codex launch or resume wrote the entry straight back into the real ~/.codex or the account's home. Both now read the per-agent predicate, which the PTY spawn env and startup already honoured. * fix(codex): turning Codex off per agent clears the real ~/.codex entry While the real-home lane owns ~/.codex/hooks.json, the legacy system-home sweep stands down. That gate read only the global switch, so turning Codex off per agent ran remove() with the sweep still suppressed and left Orca's entry in the real ~/.codex. The gate now reads the per-agent predicate, the same as turning every hook off. * test(codex): cover the system ~/.codex sweep gate for Codex turned off The gate that lets the legacy system-home sweep run was an inline closure in startup, so reverting it to the global switch left CI green. It is now a pure function beside the gate it feeds, with a table test and a remove() test on a seeded ~/.codex: turning Codex off strips Orca's entry and keeps user hooks; with Codex on the entry stays. * fix(cli): keep the agent-status hooks predicate loadable by the packaged CLI The CLI's prepare-codex handler imported the predicate from src/main, but the Electron build rebuilds out/main from its declared entries only, so the packaged `orca agent hooks` commands could not load it (package jobs and the CLI bundle-parity test were red). The predicate reads only settings, so it now lives in src/shared, which the CLI compiles itself. |
||
|
|
8f4a740b90 |
fix(daemon): roll Codex no-daemon shell launch into a fresh v37 daemon (#23907)
Terminal daemons survive app updates, so new tabs keep spawning from the old v36 daemon and never get #23900's Codex shell function. Bump to v37 so new tabs move to a fresh daemon; v36 owners stay attachable. |
||
|
|
c1628b4e65 |
feat(orchestration): deliver worker results to a structured chat coordinator (#22631)
* feat(orchestration): deliver worker results to a structured chat coordinator
Resolve a Run's handle-less session coordinator and a session:<id> mailbox to
the live session, wake an evicted session for the delivery, redrive a session's
own mail on its idle edge, route a terminal view's pointer through its PTY, and
accept session:<id> (or a bare Orca session id) as a recipient.
* test(orchestration): pin coordinator delivery through the real session host, wake, idempotence and session addresses
* test(orchestration): type the coordinator mail fixture's attach params
* fix(orchestration): refuse session recipients with the caller codes, and treat a worker without its identity as undeliverable
* test(orchestration): pin a chat's terminal view reading the chat's coordinator mail
* test(orchestration): read coordinator mail fixtures through checked guards instead of assertions
* fix(orchestration): name a structured session's CLI by $ORCA_CLI_COMMAND in its pointer turn
* fix(orchestration): render the pointer's CLI invocation for the shell the session runs in
* fix(orchestration): address a session recipient where its check reads, so a structured worker gets its mail
* test(orchestration): pin the runtime's own idle-edge redrive wiring; say a released session is not running, not ended
* fix(orchestration): point mail that has not been pointed, not mail nobody has acked, naming the ack a held batch needs
* feat(orchestration): hand a /clear-replaced chat's Runs and unread mail to the session that replaced it
* feat(orchestration): adopt a /clear predecessor's Runs at the clear's commit, the edge its replacement's own status misses
* fix(orchestration): log a wake that could not resume a session, instead of retaining silently
* test(orchestration): give coordinator mail waits a budget that holds under a loaded parallel run
* refactor(orchestration): narrow a retained pointer's dispatch state by type guard instead of a cast
* fix(orchestration): give back a pointer whose admitted turn never ran
A pending send stamped its rows delivered and dropped its operation row, so a
provider that died before echoing left the last result pointed at nobody. The
lane now awaits the admitted turn's settlement: accepted consumes the claim,
anything else returns the rows and drops this send's operation row, so the
re-point on the next edge is a new send rather than a replay of unknown.
* fix(native-chat): keep a committed /clear from failing on its replacement observer
The observer runs after the clear's durable commit; a throwing adoption turned a
committed clear into a failed RPC. It is now best-effort and logged, like the
status feed's observer, and the successor's idle edges re-derive the adoption.
* test(orchestration): pin /clear adoption across a chain of clears and a predecessor's check
A session cleared twice before any edge hands both predecessors' Runs and mail
to the end of the chain. A predecessor still live in the clear's tail reads
none of the re-addressed mail: a session's direct mailbox is consume-on-read
and holds no replayable batch.
* test(orchestration): type the pending-settlement host test's send input without an assertion
* fix(orchestration): keep a chat's orchestration address across /clear by deriving its lineage
A chat's orchestration address is now the first session of its /clear lineage. Every session of the
lineage resolves to that one actor when it acts and when it is reached, and delivery goes to the
lineage's live session. Nothing is rewritten at a clear, so the predecessor-adoption path is gone:
the commit-edge observer, the idle-edge rebind, and the unread-mail re-address. That path could
unbind the successor's own Run and orphan all but one Run of a chain.
The idle edge now opens the orchestration database through its lazy getter and logs when it cannot,
instead of reading a field that stays null until the first orchestration call after a restart.
* fix(orchestration): give back a structured pointer claim an earlier process left open
A pointer the host admits as pending stamps its batch delivered, and only an in-memory settlement
waiter gives it back if no turn ran. A process that died in that window left the batch stamped with
nothing to release it, so the last result on that mailbox was never pointed again. When the
database opens, every surviving pointer operation row is from an earlier process: its stamped batch
is found by the row's fingerprint, released, and the row dropped, before the restored-mailbox scan
points it again. A row whose batch was never stamped keeps its id for the retry.
* test(orchestration): pin that a cleared chat's sends carry its conversation's address
* fix(orchestration): open the orchestration database at an idle edge only when it already exists
A profile with no orchestration database has no mail to redrive, so a structured chat's idle edge no
longer creates one, and says nothing. An existing database is still opened lazily there.
* fix(orchestration): read a chat-coordinated Run's session through the actor's generation
A handle-less Run names its coordinator session only by an actor that still counts at the Run's
current generation, the same rule every other binding read uses; an actor an older binary's rebind
or unbind left behind no longer routes the Run's mail. A test pins that a cleared chat's run-create
and run-use write its conversation's root actor at the Run's current generation.
* refactor(orchestration): address a session's conversation by its bare root Orca session id
Carries the Orca session id rename into coordinator delivery. A session's orchestration
identity holds its conversation's bare Orca session id, the /clear lineage root's, and
its mail address is derived from it by formatOrcaSessionAddress; a Run a cleared chat
creates or uses stores that bare root id. A session recipient carries the parsed id and
its address as distinct types, a handle-less coordinator's session is read through
currentRunCoordinatorOrcaSessionId, and session ids read from records or PTY bindings
are checked with isOrcaSessionId before they become an identity. The session address
prefix comes from the one exported constant.
* refactor(orchestration): canonicalize a cleared session through the one id hook and the party resolver
- canonicalOrcaSessionId now walks a session's /clear lineage to its root; the
parallel session identity and lost-worker rule are deleted, so the caller
resolver, recipient routing, reach and idle-edge mailboxes all resolve a
session through resolveOrcaSessionParty.
- A Dispatch row's assignee_orca_session_id goes through the same hook.
- The terminal-view delivery lane is gone with the terminal handoff: no PTY is
bound to a session, so a chat's mail is always a session turn.
- Pins a send to a Run-less chat's session address after a restart, when the
send must start the agent-session host before routing reads its record.
* fix(orchestration): point a structured session with the PTY lane's exact text
A chat or structured worker is now told what a terminal agent is told: the pointer is
formatMessagePointer with the CLI name the PTY lane resolves for a local terminal (orca, or
orca-dev in a dev build), with no shell-specific invocation and no ack lesson. The lane still
excludes the batch a reader holds unacknowledged and points newer mail; that stays host-side,
and the reader's own check replays the held batch and names its ack as it does for a terminal.
* fix(orchestration): deliver a cleared structured worker's mail to its live successor
A terminal keeps its handle across /clear; a structured worker's successor now does the same.
Mail at the worker's handle, its dispatch mailbox, and a Run it coordinates resolved to the
session minted for the worker, which /clear replaced. Each now walks the /clear lineage forward
to the live session, which the caller resolver already treats as the worker.
* test(orchestration): compare a chat's pointer turn to the PTY lane's text for this build's CLI name
* refactor(orchestration): resolve the local CLI name once, for the PTY lane and the structured lane alike
* fix(orchestration): let a /clear-ed chat restate its address, placed by the host's lineage
The CLI entry compared a restated --from/--terminal with the injected session id as a
plain string, so a cleared chat restating the address it had before the clear (its lineage
root, the address it keeps) was refused. Only the host's session records know the lineage,
so a session address that is not this session's own spelling is now sent as the caller
param, where the host's canonical-id check accepts it or refuses it before any effect.
Plain restatements are still dropped and any other name is still refused at the entry.
* test(orchestration): read the coordinator journal through the async snapshot, and wait for the held turn's handover
Main made journalSnapshot async and delivers an accepted send once the host hands it over, so the fixture awaits the snapshot and waits for the provider's turn before echoing it.
* refactor(orchestration): point a chat whose agent is not running through the plain send
Main's host no longer has hold/release: an accepted send starts the agent itself. The
pointer lane's wake step called host.hold, which no longer exists, so it is deleted
from the pointer host, the delivery lane and their tests. An idle or evicted chat gets
its pointer through the same send a user message takes; the claim is still consumed
only on accepted and given back otherwise.
* fix(orchestration): leave a chat whose provider died stopped instead of respawning it for mail
A pointer whose provider died before echoing it is given back. The death's own status
edge then redrove the mail, and since a send starts the agent, a provider that died on
every turn was restarted about once a second for as long as the mail was unread. The
pointer lane now reads, on every attempt, whether the session's latest send never ran
because its provider exited or could not start, and holds the mail until a later send
runs. Every trigger passes through that gate: a parked retry, the idle edge's re-derive
and new mail. A rejection for any other reason still points at the next idle edge.
* fix(orchestration): hold mail after a start the person must fix, placing every failure kind
A start refused for a reason only the person can fix (not signed in, history too large,
a managed-account problem) or one the host stopped because it never came is held like a
failed start: the mail waits for the person's next message, which also retries the start.
An account switch still in progress is transient, so it stays ungated and the first edge
after the switch settles points the mail. One exhaustive record places every rejection
kind, so a new kind does not compile until it is placed.
* fix(orchestration): retry a structured pointer under its own id instead of gating on the failure reason
A pointer send that failed was given back and re-sent under a fresh operation id,
so every status edge after a provider death was a new send that started the provider
again. A reason-string gate held some of those deaths, but missed a Codex crash with
turn/start in flight (it settles unknown with the connection's error), latched all later
mail after one transient death, and did not keep a user's Stop.
A retry now reuses the mailbox's operation id, which the host answers by replaying the
recorded verdict without reaching the provider. The id is re-minted only for new mail,
after a later send ran, for a row an earlier process left, or once an account switch
settles. Rows are stamped only on accepted, so the admitted-stage claim, its give-back
and the restart claim-release scan are gone.
* test(orchestration): pin that a Stop keeps a pointer unsent before the next status edge, too
* fix(orchestration): point a structured chat's mail by the same rule as a terminal's
The chat lane pointed newer mail past a batch its reader had checked and not
acknowledged, while the terminal lane skips a mailbox until that batch is acked. A chat
now waits for the ack the same way a terminal does, and the lookup that let the chat lane
filter the held batch out is removed.
* fix(orchestration): refuse a session address that gate-list or task-list --run would drop
The CLI entry lets a `session:` address that is not the session's own spelling through
for the host to place, but gate-list and task-list send no caller when --run names the
Run, so `--from session:<other>` was silently ignored. With --run they now refuse it
(consumer_fenced) before any request, the same as a conflicting terminal handle.
* fix(orchestration): keep a failed mail redrive from skipping a chat's first-turn workspace rename
A structured session's status callback redrives its mail before the first-turn workspace
auto-rename, outside any try, so a database error there threw past the rename. The redrive
now logs its failure and returns.
* chore: take main's pnpm-lock.yaml the merge of origin/main left stale
* fix(orchestration): give a stamp or park decision its own variant so the send branch narrows
* fix(orchestration): re-mint a held structured pointer from facts that cannot strand it
A pointer row whose id had no submission in the journal was always resent under that id,
before any re-mint test ran. After a rewind rebuilt the journal, or a send refused before it
was recorded aged past the host's 24h admission window, the mail was held forever: neither
the person's next turn nor a restart pointed it again.
The re-mint tests now run first. "The agent has run since" is any accepted send submitted
after the row was minted; "an earlier process minted it" is a row whose id this lane did not
send, not a wall-clock comparison a clock step could fool; and a row the host never recorded
is re-minted once it is too old for the host to admit. The account-switch exception is gone:
nothing tells the lane when a switch ends, and each outside edge during one added another
pointer and failure to the chat. A parked pointer now retains as turn-unsettled.
* fix(orchestration): let the host check a session caller that gate-list or task-list --run names
|
||
|
|
9420d49bcb | fix(terminal): run Codex in Orca terminals without the shared background server (#23900) | ||
|
|
3c1af16e8b |
perf(usage): persist a scan's analytics session IDs in one write (#23807)
* perf(usage): persist a scan's analytics session IDs in one write Minting identities one at a time rewrote and fsynced the whole identity file per new session, so a first scan of N sessions did N durable writes and O(N^2) serialization inside the scan (~44s at 5,000 sessions). Batch lookups so each scan does at most one write. Fixes STA-8749 * refactor(usage): resolve IDs before loading snapshots; skip empty batches |
||
|
|
31012aeb09 |
test: remove assertion-free probes, copied inventories and export-shape checks (#23816)
Second audit wave, targeting three more junk patterns: - assertion-free cases that run code and assert nothing, so they pass no matter what the code does; - inventory literals re-typed from a production declaration, where the only way the assertion can fail is someone editing one of the two copies; - export key-set and export-shape loops (`typeof x === 'function'` over every export) that restate what TypeScript already enforces. Yield is much smaller than wave 1 on purpose: the assertion-free scanner has a high false-positive rate, because many flagged blocks assert through a shared helper or their oracle is "this must not throw". Those were kept. `mobileWebCheckArgs` in `config/scripts/run-mobile-web-app-checks.mjs` is de-exported — after the inventory comparison went away, nothing outside the module read it. |
||
|
|
c49388cd33 |
fix(native-chat): Stop is there from the moment a message is sent (#23026)
* refactor(native-chat): remove the unused terminal handoff No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths. * fix(native-chat): never let the pre-stop snapshot hold a chat's stop Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop helpers only the terminal handoff called `claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and `queryWindowsProcessRowsFresh` lost their last caller with the handoff. The fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`, the teardown path that still depends on that contract. Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): stop citing the removed handoff in lifecycle comments Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stalled snapshot drain without a cast Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a start dead before proving owes no settlement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(terminal): remove the agent-session PTY write gate The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop the transcript helpers only the handoff called appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop calling a starting chat "mid-handoff" A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stand-in roster decoder without a cast Co-Authored-By: Claude <noreply@anthropic.com> * refactor(codex): name the pinned rollout lookup for what it does With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow. * refactor(native-chat): type the owner-status reply as the host sends it The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged. * refactor(native-chat): normalize terminal-handoff lease values once at decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind. * refactor(native-chat): stop threading the owner kind through a reservation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it. * test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write. * fix(native-chat): name a chat write by its target, not the owner generation A write carried the fence of the last frame the pane read, and the host refused it unless that fence was still current. An idle release and the restart after it each move the fence, and the release publishes nothing, so a send after a release was refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a cold start was refused as stale. Every write already names what it acts on: a send its conversation, a cancel its turn, a prompt answer its item revision, a rewind its epoch; an option is last-writer-wins. So admission stops comparing the client's fence, and the rebase that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it. The writer-lease check stays, and so does the attach's compare-and-swap. Frames now stamp the fence read when each frame is sent instead of a copy each subscriber kept, which went stale on the same release. * fix(native-chat): every journal append reaches the chats that are open A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted. * test(native-chat): an epoch replacement reaches the open chat * test(native-chat): each row reaches an open chat once, and a live handle enters only through the map * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. * perf(native-chat): a publish behind a delivered commit reads nothing Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read. * test(native-chat): state why the teardown test's fake journal is safe to cast * docs(native-chat): say mutation admission checks only the writer lease * docs(native-chat): drop the send rebase from comments that still described it * fix(native-chat): a message is accepted, then delivered A send to a chat with no running agent restarted the agent inside the send call, before the message was recorded, so the client waited for the whole start and a failed restart refused the message. Claude held prompts sent during startup, and those could settle as "unconfirmed". A send is now accepted inside the session's serialized queue: one ledger row and one submission row marked handoverRecorded, published, answered pending. A per-session delivery loop exists while a message is queued. It starts the agent through the same serialized attach a hold uses, waits outside the queue for a Claude child to prove its start, and hands the oldest queued message over as its own serialized step, writing dispatch{pending} before the adapter call. A start it needed and did not get writes one error-tone row and rejects every queued message with the same words; a start Stop cancelled writes none. Settlement follows from the rows. A queued message is provably unwritten, so a close, an eviction or an exit rejects it. A handed-over message stays in doubt. A queued row at or below the sequence a handle found when it opened was left by an earlier process and is rejected at open, with no latch. Stop withdraws queued messages with no writer lease and no fence. An attach failure keeps the conversation open, and the attach adopts its journal. Owed work counts the loop and queued rows. A compaction or rewind found prepared when a conversation opens was started under a child this process no longer has, so the open settles it rather than leaving it to refuse every send until a view attaches. The open cursor is scoped to its epoch, because sequences restart when an epoch is replaced. Deleted: restart-before-admission, recordFailedRestart, the fence rebase, Claude's startup gate, the attach's forget on failure and its own crash boundary. Clients without agent-session.accepted-send.v1 get their reply held until the handover; the desktop and paired desktop lists advertise it. * fix(native-chat): settle queued messages only for the child that ended A child that proved its start and then exited before its message was handed over left the message queued: the exit settlement returned early when nothing else was in flight. Delivery then started another child for it, and a child that died the same way started another, without end and without a row. A retried settlement for an earlier generation, run by the attach that delivery started, did the opposite: with that generation's turn unfinished it rejected the message queued for the child being attached. The settlement now takes the rejection for queued messages from its caller. The unexpected exit and the eviction pass one, and it applies even with no other work in flight; the retry for an earlier generation passes none. * fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. * perf(native-chat): the recovering open reads the journal once Every conversation open now goes through the recovering open, including the read restore of every chat at startup, which used to replay its journal once. The recovering open replayed it twice: once to probe it and again inside the open. The probe is now handed to the open as its load. * fix(native-chat): an attach that fails after indexing its child leaves no child behind A failed attach now keeps the conversation open, but a failure after `onAttached` indexed the child (the rewind or compaction recovery, or the attach's own success record) left that entry claiming a child the failure path had already released. The next send found the phantom, skipped the start, and wrote at a fence the journal had moved past, so the message stayed queued for good. The entry now drops the released child and its event sink, and follows the record's fence, as a failure before indexing already did. * fix(native-chat): a withdrawn message shows no error, and a rejection outlasts the send's answer The error strip for a message the host accepted and then did not deliver matched the entry before the outbox reconciled, so a Stop's withdrawal, which the reconcile drops, showed "Orca could not send your message" with nothing to retry. It now reads the reconciled entry. A rejection the journal records before the send's own pending answer lands is final as well: that answer no longer puts the entry back to dispatching with no Retry. * fix(orchestration): a structured worker whose agent outlasts the preamble wait is left unknown, not torn down The preamble waits for its submission to be delivered while the worker's agent starts. When that wait ran out it threw operation_unknown, and the failed-start teardown then closed the session, which rejected the very preamble the host was about to deliver. It now reports a turn start nobody observed yet: the worker is start-unknown with its session kept, the host delivers the preamble when the agent starts, and the worker's report settles the dispatch as for any unobserved start. The receipt no longer suggests reading a screen a structured worker lacks. * fix(native-chat): a message rejected while its chat was closed reads as not sent A remount reads an entry it left dispatching as unconfirmed. When the journal had rejected it meanwhile, as a failed start or a quit now does, the reconcile left it unconfirmed: it blocked every later message behind a Retry and no reason, and the delivery probe, seeing the journal already answered, never ran. The reconcile now settles it as rejected like a dispatching one. * test(orchestration): name why the readiness settlement fakes are cast * fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent * docs(native-chat): drop the fence from the admission the send effects run behind * docs(native-chat): give the fence move on release the reason that still holds * docs(native-chat): stop citing a write fence check in launch and mailbox comments Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease. * refactor(native-chat): the provider child is its own record A conversation now outlives any number of provider children, so the child is one record on the conversation's entry instead of five loose fields beside its journal. It is written in one place: indexed only once an attach has fully succeeded, and ended through one function that an exit, a failed re-attach, a Stop and an eviction all share, matched on the child's generation and fence. - A failed attach writes no child, so there is nothing to unwind: the field unwind and the fence patch after it are gone. - Conversation writes read the record's fence, the way mutation admission already does; a child's own writes use its fence. The four stored-fence patches, and the settlement retry's overwrite of the conversation's fence, are gone. - The owed wind-down is its own tombstone, carrying the child it is owed for, and is no longer dropped when an attach replaced the whole entry. - Stop on a child still proving its start stops only the child: its lease goes back and the chat is told it is idle, but the journal, the holders and the readers stay. Close is that stop plus the conversation's close. - The settlement retry uses the conversation's own journal, opened through the host's one open. * fix(native-chat): the delivery loop alone settles a message its start or child failed A queued message was settled by whichever path happened to end the child first: the loop, the unexpected exit, eviction's work settlement, the open's leftover rule, and the startup branch that rejected every pending row. That gave two failure rows with different tones for one start, a loop that could hand over to a different child than the one it waited on, and a Claude start that died while starting reading unlike every other failed start. - The loop remembers the child it waited on. At handover, if that child is gone or replaced, it reads how it ended: a Stop continues; anything else writes one failure row and rejects every queued message with the same words, then stops. A child still starting whose start the adapter says did not land fails the same way. The exit, eviction and the settlement retry only settle the handed-over and legacy rows of the child that ended. - One failure row, always an error, keyed by the start. A start a view began that dies with nothing queued writes the same row through the same builder, so a second report revises it. - The open no longer rejects leftovers; the loop's first step does, and the open wakes it. - `awaitStarted` answers why a start did not land, so the row says it even when the loop sees the failure before the exit is processed. - Quit closes every conversation the way closing a chat does: what is still queued is rejected as closed, with or without a child, and a start the loop already has in flight is waited for so the child it produces is stopped rather than left behind. * 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. * 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. * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * 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 * 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`. * fix(native-chat): a Stop that names no turn stops what the conversation has in flight Between handing a message to the agent and the agent opening its turn, there is no turn id a client could name, so a Stop in that gap was refused as "already finished" while the agent went on to answer. A cancel's turn id is now an optional precondition instead of its target: with none, the host withdraws what is queued and, when the journal still reads working, asks the adapter to stop whatever the child has in flight. Claude's interrupt is session-scoped, so it is guarded by fence and acquisition generation rather than a turn identity. Codex interrupts the turn its latest turn/start answered with until the journal shows one. A cancel that names its turn behaves exactly as before. * fix(native-chat): Stop is there from the moment a message is sent The composer showed Stop only once the agent had opened a turn, so for the second or two after a send the chat read "thinking" with no way to stop it. Against a host that takes a Stop naming no turn, Stop now shows whenever the chat reads working (a turn, a queued message, or a handed-over one still unanswered) or this client still has a message on its way. Pressing it, or Escape, first drops every outbox entry the journal does not hold yet, so nothing goes out after the Stop, then sends the conversation-wide cancel. A send already on its way reaches the host ahead of the cancel, which withdraws it there. Against an older host Stop still needs a running turn. The unconfirmed-send probe moves into its own hook so the outbox hook stays in budget. * fix(native-chat): Stop before a turn is gated on its own host capability A host that accepts sends first (agent-session.accepted-send.v1) can still predate the cancel that names no turn and would refuse it as invalid, since clients and hosts ship independently. Hosts that take that cancel now advertise agent-session.conversation-stop.v1, and the renderer shows Stop before a turn opens, and sends the no-turn cancel, only to a host advertising it. Every other host keeps a Stop that needs, and names, a running turn. The host capability probe the accepted-send hook used is generalized so both read one path. * test(native-chat): a build advertises conversation stop exactly where its cancel may name no turn * 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. * 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(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * 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): 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. * fix(native-chat): Stop reads the one working rule every session list reads While Claude retries a rate-limited request it never echoes the message, so no turn opens: the sidebar read Working from the unanswered send while the composer showed Send. The chat's working state, the host's session-list status and the host's no-turn Stop check now call one shared rule instead of three copies. * test(native-chat): a rate-limit retry pins only that no turn opens, not how its rows are kept * fix(native-chat): Stop leaves a message waiting on its Retry, and does not show for one A send that failed holds the queue until the user retries it, and one the host restarted under is parked the same way. Stop counted both as still on their way, so it showed in an idle chat and could never go away, and pressing it dropped the failed message along with its Retry. * test(native-chat): the chat's Stop and a session list read the main agent alike over their own copies The chat reduces its stream and a list reads the status feed. Driven through the real host for a rate-limit retry with no turn, a subagent still running after the main turn, and the handed-over child exiting. * refactor(mobile): the chat reads the main agent's working state through the shared rule Behaviour is unchanged: the same two terms, now from the one function the host projection and the desktop chat read. * fix(codex): a Stop naming no turn never interrupts an earlier turn It fell back to the id an earlier turn/start answered with when the latest start went unanswered, or when the journal showed a compaction Codex had not started, and reported that as stopped. * fix(native-chat): a Stop naming no turn never says a turn had already finished When the provider found nothing left to stop, for instance a turn that ended between the host's check and the interrupt, the chat got "The provider had already finished this turn." for a turn the Stop never named. It now ends quietly, as a Stop with nothing in flight does. * fix(native-chat): one Stop the host could not settle no longer refuses every later one A Stop naming no turn has one operation key per session. When the host could not settle one, it answered every later Stop under the same id as unknown until the id expired. Once the host says so, the next press is a new Stop; transport doubt still replays the same id. * 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. * test(native-chat): read Stop operation ids without a cast * fix(native-chat): a Stop whose answer was lost no longer swallows the next one A Stop that names no turn has one operation key per chat. When its answer was lost in transit, the chat kept the id, so every later Stop replayed it; the host answers a replay as already handled, so for up to a day Stop stopped nothing. The id is now dropped once the call settles, however it settles. A second press while the first is still on its way still shares its id. * refactor(native-chat): a Stop naming no target keeps its operation id only for its own call The chat kept each write's operation id per payload across calls, and dropped it only on some settle paths. That is right for a write naming what it acts on, but a Stop naming no turn, and a stop of every background task, share one payload with every later one, so any path that kept the id made the next Stop replay as already handled and stop nothing. One path was still open: an answer that arrived after the chat moved to a new fence. Whether a write names its target is now decided once, before its id is picked. One that names none keeps its id only while its call is in flight, so a press made meanwhile joins it, and releases it when the call settles, however it settles. The release runs only while the key still holds that call's id, so a joined call settling late cannot drop a newer one's. This replaces the per-path exceptions for a thrown call. * test(native-chat): read the Stop fences without a cast * test(native-chat): pin the new id for a named cancel the host could not settle After the Stop naming no turn moved to a per-call id, the only test of the unknown-refusal release was gone, and the half that stays, for a cancel naming its turn, could be removed with every test green. * fix(native-chat): a Stop pressed after a new message stops it, even while the last Stop is unanswered A Stop naming no turn shared its operation id with any press made while it was still in flight. The host runs a chat's writes in order, so a message sent between two presses was accepted after the first Stop ran, and the second press replayed that Stop as already handled and left the message running, although the chat had already withdrawn it from the outbox. A write naming no target now gets a new id on every press and is never kept, so each Stop acts on whatever is running when the host reaches it. A write naming its target keeps its id exactly as before. A double press can ask the provider to stop the same turn twice, which it tolerates. * fix(native-chat): Stop no longer blinks off as Claude opens the turn for a message Claude's echo of a sent message both answers the send and opens its turn. The echo settled the send first, so the host published the message as answered one frame before the turn it opened, and for that frame the chat read nothing running: Stop turned back into Send, and Working blinked off in every session list, for tens of milliseconds on each turn. The echo now settles the send after the turn it opens has been emitted, so the running turn is published first. * fix(native-chat): a message a Stop withdrew comes back to its sender's composer A Stop withdraws every message the host holds but has not run, and S also drops the ones this client had not handed over yet. Either way the message left the chat and its text survived only in a hidden journal row and the in-memory ArrowUp history. The sending client now puts the withdrawn text and images back in that pane's composer, after whatever is typed there. Withdrawn is read from the rejection reason through one shared check, which the outbox reconcile now uses too. The composer is written before the entry leaves storage, so a failure between the two repeats the text instead of losing it, and an entry storage no longer holds is never given back again, so a replay, a second view or a remount restores it once. Only this client's outbox holds the entry, so other viewers still see the message disappear. A failed Stop withdraws nothing on the host and gives nothing back. * fix(native-chat): withdrawn text put back during an IME composition is not lost While the IME owns the field, the composer ignores a programmatic draft, and the next composed keystroke wrote the draft without the restored text, after its outbox entry had already been dropped. The composer now holds text appended mid-composition, keeps it in the cache after each composed write, and shows it once the composition settles, the way attachments that land mid-composition already wait for it. * test(native-chat): pin that only a withdrawn message comes back to the composer * test(native-chat): set up the composer's window API for every describe in the composition-race file * docs(native-chat): note that the withdrawn check reads the legacy reason until a typed category lands * test(native-chat): pin that text put back mid-composition shows once, even beside a mid-composition clear * fix(native-chat): land a late settlement from a streamed turn's end after that turn's rows A settlement that says a streamed turn ended waits for the session's event sink to drain before writing its dispatch row. The journal reducer still refuses to overwrite an accepted or rejected send. * fix(codex): settle a send from the end of the turn Codex answered it into The turn/start answer names the turn that holds a send. The send's echo entry now keeps that binding, in memory only. If the bound turn is interrupted without echoing the send, the send is withdrawn: Codex clears a turn's pending input on interrupt, so the model never saw it. If the turn fails first, the send is rejected in Codex's words. A completed turn settles nothing, since Codex records pending input when it finishes and the echo is still due. An answer read after its turn already ended is settled by that end. The echo is still the acceptance and carries the item key. * test(codex): a send settles from the end of the turn Codex answered it into The fake Codex keeps 0.157's turn bookkeeping, and can deliver the turn/start answer after turn/started or after turn/completed. The tests cover: - a Stop before any echo withdraws the send, and the working rule reads idle; - a steered follow-up is withdrawn when the turn is interrupted; - a failed turn rejects the send in Codex's words; - a completed turn leaves the send to its echo; - a normal echo and a late echo; - two steered sends in one turn; - an answer read after the turn ended; - a timed-out answer; - child-thread turns; - how a binding dies. * refactor(native-chat): drop the stream flush before a late turn-end settlement Nothing reads the order of a dispatch row against the turn's terminal row: the reducer keeps a settled send terminal and the working state is derived from both. The echo acceptance on the same path never waited either, and the wait could drop the settlement on a failed sink barrier. * fix(codex): settle a failed turn's sends at its end, not at its error Codex keeps a failed turn's pending input and records it after the error frame, before turn/completed. Settling at the error rejected a steered follow-up the model had in fact received, so a Retry would send it twice. * docs(codex): say a completed turn echoes what it took before it ends Codex records a completed turn's pending input before `turn/completed`, so a bound send that turn never echoed is left for recovery, not awaiting an echo. The comments and one test title said the echo was still due. * test(codex): settle a send whose answer is read after its turn failed or completed A failed turn that ended before the answer rejects the send in Codex's words, once; a completed one leaves it admitted and still armed for its echo. * refactor(codex): read a failed turn's reason with the typed thread-fact reader * fix(native-chat): a Stop that names no turn and ends nothing says why The host sends a Stop naming no turn to the agent only while the chat reads working. When the agent ended nothing, the Stop wrote no row, so it looked ignored. It now writes one: Stop could not reach the agent, in the agent's own words when it refused the interrupt. * fix(codex): hold a cold send until Codex opens its turn, and stop the turn it opened Codex answers turn/start before it opens the turn, and refuses an interrupt until then. A Stop queued behind a cold send ran in that gap, named the answered turn, and was refused. The send's handover now lasts until Codex opens that turn, or provably will not: the turn ended, the primary thread stopped running, or the child ended, bounded by the turn/start deadline. A steered send, whose turn is already open, does not wait. A Stop naming no turn now interrupts the turn the journal shows, else the primary-thread turn Codex reported started and not yet ended. The per-start answered id is gone: it was never cleared at a turn's end. * fix(native-chat): give back a send already on its way only when the host withdraws it Stop took the in-flight send out of the outbox and put its text back in the composer at once. The send still landed ahead of the Stop, so the chat read working for a moment before the host withdrew it, and a send the agent had already taken came back as well. The in-flight send now stays until the host answers it, and the withdrawn-message restore gives it back from that answer. * refactor(native-chat): read a Stop's withdrawal through the rejection classifier dispatchWasWithdrawn matched the legacy reason string. It now asks the classifier, which reads the typed fact first and keeps that string only as its own fallback, so a withdrawal written as a fact with a sentence is still given back to the composer. * fix(native-chat): a Stop Codex took but Orca could not confirm is not reported as reaching nothing When Codex acknowledged the interrupt but Orca could not verify the turn's processes ended, a Stop naming no turn wrote "it had no turn running to stop", though the turn then ended as interrupted. The adapter now says the Stop was taken but unconfirmed, and the row says Cancellation was not confirmed. * fix(codex): a held cold send never delays closing the chat or quitting A cold send's handover waits for Codex to open its turn, and that wait sat in the session queue ahead of the close and quit eviction, so either could wait out the 30 s request deadline. The host now releases those waits before it queues a close or starts quit teardown, and the adapter releases them when it is asked to close the child and on every exit, including one whose end publication is backpressured. * fix(native-chat): a refused Stop says the agent declined, and names it "Stop could not reach the agent" was wrong: the agent was reached and declined. The row now reads "Codex had no turn running to stop." or "Codex didn't stop: <Codex's words>.", naming the chat's agent. * fix(codex): a Stop waits for the turn Codex answered to open; the send no longer does Codex answers turn/start before it opens the turn and refuses turn/interrupt until then, so a Stop naming no turn in that window was lost. The send's handover used to wait for the turn to open, and a close or quit needed its own release to get past that wait. Now only the Stop waits. A Stop naming no turn, finding no journal turn and no open one, reads the turn Codex answered the latest pending send into and has neither opened nor ended, and waits for it: bounded at 5 s, under the quit eviction budget, and ended by that turn opening or ending, the thread going idle or failing, or the child exiting. If the turn opened it is stopped; otherwise the Stop reports that Codex had no turn running. The send returns at Codex's answer, so a close or quit with no Stop pending is never delayed, and the release plumbing through the adapter, router, close and quit is gone. * fix(codex): the Stop's wait reads a stopped thread from the thread-facts reader main kept --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4da485cac1 |
fix(native-chat): render the terminal-backed chat through the structured chat's turn status (#22090)
* fix(native-chat): render the terminal-backed chat through the structured chat's turn status
A terminal-backed chat (grok and omp always; Claude and Codex whenever the
structured lane is refused) could not say what its agent was doing. A
working agent drew three bouncing dots with no elapsed time, and an agent
stopped on a prompt that only its terminal showed drew nothing, so it looked
idle. A pending AskUserQuestion row read "Asked:" while the agent was still
waiting, because the pane treated the wait as the end of the turn.
The pane already held the facts: the host-stamped working epoch and the
hook's waiting/blocked state. It now feeds them to the same message list
props and components the structured lane uses, instead of opting out with
showTurnStatus={false}:
- The turn runs on while the agent waits on the reader, as a structured turn
does behind its prompt card, so the "Working for" bar keeps counting and
the live line yields to the wait.
- A wait the pane can draw as a card (approval or question) is shown by the
card; a wait only the terminal shows is reported by the tail row the
structured lane already uses for a pending question
(NativeChatAwaitingInputRow).
- The hook wait is reconciled against the transcript's terminal marker, like
hook 'working' is, because an interrupt at the prompt fires no hook.
showTurnStatus meant nothing once both lanes passed it, so it is gone, along
with showLiveTurnActivity (replaced by the awaitingInput fact), the
structuredActivityUi flag it fed to tool rows, and the transcript-guessing
typing indicator (native-chat-typing-indicator.ts,
NativeChatTypingIndicatorRow.tsx), which had no other consumer.
The interactive card's derivation moved into
useNativeChatInteractivePromptCard so the view can tell a wait the card
answers from one it cannot.
* test(native-chat): seed the hook wait through the store instead of a cast
* fix(native-chat): say "Awaiting user input" as a whole phrase when no question is named
A wait the pane cannot draw as a card, and a question whose text could not be
parsed, both drew "Awaiting user input:" with nothing after the colon. The row
now reads "Awaiting user input" when it has no question to name, in both chat
lanes. The pending-question view test now counts awaiting rows by attribute so
a second, unnamed row cannot slip past it.
* perf(native-chat): read the prompt's tool name only while a prompt is pending
Deriving the prompt card moved from the card into the pane's view, and with it
a store read of the status row's tool name, which changes on every tool call.
Every tool call therefore re-rendered the whole terminal-backed pane and its
transcript list, where before only the card re-rendered. The tool name only
matters beside a prompt, so read it only then.
* fix(native-chat): keep the terminal lane's turn clock and folds across a remount
Answering a prompt only the terminal shows means switching the pane to its
terminal, which unmounts the chat. On return the "Working for" clock restarted
from the agent's current state (seconds after the answer), and every turn the
pane had folded behind "Worked for N" came back unfolded with no bar.
- The running turn now counts from the start of the hook's unbroken run of
mid-turn states under the current prompt (read from the status row's state
history), not the current state's own start.
- Finished turns take their duration from the transcript's own timestamps
(prompt to the agent's last row or its interruption), so history turns fold
like a structured chat's. The latest turn still relies on what the pane saw.
* fix(native-chat): date the terminal lane's turns by the main agent and skip harness notices
Two ways the terminal-backed chat showed a wrong turn status:
- A background task or subagent keeps the status row 'working' after the
main agent finishes, so the row's own start time carries into the next
turn. A prompt sent while a background dev server ran showed "Working for"
counted from the earlier turn. The clock now reads the main agent's own
state and start time where the row carries them.
- Harness notices (task notifications, reminders) are user-role rows in the
transcript that the chat does not draw. The finished-turn derivation
treated them as new prompts, so a running turn was reported settled and
folded its steps while it still ran, and a finished turn's "Worked for"
stopped at the notice. They no longer start or end a turn.
* feat(agent-status): stamp each turn's start on the host
The hook server now records turnStartedAt on a status row when the main
agent's own turn-opening event arrives (the per-provider new-turn
classifier the observation boundary already uses; not a replay, a child
event or an identity-only row). Other events carry it; a session
boundary clears it, and a settled main agent running again with no
opening event drops it rather than count from the finished turn.
It rides the snapshot and the live push as an optional field, persists
with the row, lands on the renderer entry (kept within one state for
writers with no turn clock), and is part of the paired-client
projection key and equality. Old hosts send nothing; readers fall back
to stateStartedAt.
* fix(native-chat): time the terminal lane's turns from the host's turn stamp
The running clock reads the host's turnStartedAt, so a remount, a reload
(which starts with no state history) and child work holding the row open
no longer move it, and the same prompt sent again is a new turn. The
stateHistory walk and its prompt-equality rule are gone; an old host
falls back to the current state's start.
The latest turn is settled from host facts: once the main agent is done
(outside a session boundary) its duration is the done stamp minus the
turn start, so it folds after a remount. When the host went quiet
mid-turn (the store's staleness dropped the row) the pane passes null,
so a still-waiting turn no longer reads "Worked for 30m" and folds.
Older turns keep their transcript durations.
* test(agent-status): pin the host's turn start on the live status push
* fix(native-chat): keep omp's turn working while its latest row is a tool call
The terminal-backed chat settles a hook 'working' turn when the transcript's
last row is an assistant row written after the turn began, a recovery for a
Stop hook the host can miss. omp writes a timestamp on every row and has no
transcript turn markers, so every tool call mid-turn tripped it: the pane
dropped its "Working for" bar and showed Send instead of Stop while the
command ran.
omp's own runtime delivers its turn end (agent_end, retried until the host
takes it and held back while the run continues), so its hook 'working' is
never a dropped Stop. The prose recovery now skips it.
* test(native-chat): read the clock once in the host-ended remount test
The test stamped the row's done time and the turn start from two separate
Date.now() reads, so whenever a millisecond passed between them the expected
"Worked for 1m 30s" came out as 1m 29s. It failed that way once under load.
* test(sync-runtime-graph): carry the turn stamp in the hot-path projection reference
The hot-path suite compares the agent-status projection against its own copy of
the serializer; that copy lacked the new turnStartedAt key.
|
||
|
|
6e1b7e7fa3 |
test: remove junk tests that assert source text instead of behavior (#23815)
Deletes 101 test files and trims 112 more, all matching documented junk patterns: exact source/import/string greps, copied inventories and export lists, duplicate invocations of a contract another test already owns, typeof-shape checks TypeScript already enforces, and self-comparisons. The largest group read a production `.ts` file and asserted on its text — for example a TaskPage test that required the source to contain `selectedRepos.find((r) => r.id === newIssueRepoId) ?? selectedRepos[0] ?? null`. Any behavior-preserving rename broke it; no behavior change ever did. Production-side follow-through: exports that only these tests imported are de-exported or deleted, stale comments pointing at removed censuses are dropped, and the reliability-gate registry, `cloud/package.json` test lists, and orphaned source-reading helpers are updated so nothing references a deleted file. Two files kept their real coverage and lost only the census scaffolding: `agent-status-producer-census.test.ts` now drives all five producers end to end instead of grepping the source tree, and `config-toml-trust-stale-writes` replaces an export-list parity check. |
||
|
|
36c473ea1a |
fix(runtime): wait out Codex 0.157's startup screen, and stop at Codex's startup dialogs, before typing a worker brief (#23745)
* fix(runtime): wait for Codex's live chat before typing a worker brief Codex 0.157 draws a provisional startup screen (header reads model: loading) and discards typed input while it starts its shared daemon behind it; a fresh Codex home makes that window seconds long, so worker-start pasted briefs that were truncated or never submitted. Codex 0.158 dropped the header labels Orca matched, so worker-start stopped seeing Codex as ready at all. Readiness now requires Codex's live chat on both layouts: the provisional header vetoes a text match unless the live status row is already painted, and 0.158's greeting layout counts once that status row appears. Codex 0.158's model announcement dialog is reported as a blocked prompt instead of receiving the brief. * fix(runtime): recognise Codex's provisional screen from the text copy and the screen probe Live worker-start on a fresh Codex 0.157 home still typed during the daemon start: Codex leaves its alternate screen for that window, so the live screen showed no header and the screen-based veto never fired. The text copy keeps the provisional header until the live chat paints its status row, so the veto now reads it there. The tui-idle visible-screen probe used the bare text rule on the rendered screen; it now goes through the same body rule. * test(daemon): register the new Codex captures' known serializer divergences The serialize round-trip replay picks up every fixture under runtime/__fixtures__, and the three new Codex 0.157/0.158 captures showed 48/9/8 "new-fail" checkpoints against an expected 0, turning CI red. They are the existing live-pen colour leak on restored cells, the same class as the other Codex and DSH entries; this branch changes no serializer code. * fix(runtime): keep Qoder off the Codex screen probe change; drop an unbacked row filter - The tui-idle visible-screen probe now classified Qoder panes with isQoderComposerReady, which skips the working veto evaluateTuiIdle applies first. Qoder paints its composer mid-turn, so an adopted Qoder pane whose hooks said "working" settled the wait immediately. Only Codex and unknown panes take the body rule there; every other agent keeps its old verdict. - The live status-row check skipped rows containing "waiting for startup", a string Codex 0.157/0.158's TUI never prints. The line-folded text copy keeps a whole screen on one line, so the filter could only ever veto the real status row. It is now a bounded includes() with no split. - Lowercase the wait text once in isKnownReadyPromptBody. - Restore the per-frame "screen never takes a settled header away" check, guarded on the provisional veto, instead of checking the final frame only. * fix(runtime): stop reporting Codex 0.158's model announcement once it is answered The announcement's choices stay in the text copy after the user answers it, and the existing dismissal check needed the model:/directory: labels that 0.158's header lacks, so tui-idle waits and the agent-status query kept reporting codex-model-migration-prompt over a live chat. Codex repaints its whole screen, header included, when a startup dialog closes, so the header after the dialog now marks it answered. Also corrects the live-chat marker comments: the middle dot also comes from the daemon session's agents hint row and the warnings notice, not only the status row. * docs(runtime): note that Codex startup dialogs also draw the live-footer dot * fix(runtime): recognise Codex 0.157/0.158 startup dialogs by the rows they really print Codex 0.157 and 0.158 no longer print `Press enter to continue/confirm` on their startup dialogs; they print key rows instead (`enter continue · esc skip`, `enter confirm · esc skip`, `enter/esc continue · ctrl+c quit`). The update, hooks-review and model-migration matchers still required the old wording, so none of these dialogs was reported as blocked. On 0.157 the dialog's `·` also satisfied the live-footer check, so a tui-idle wait read the update dialog as ready and worker-start would type the brief into it, where Enter picks "Update now" (npm install -g, Codex exits). On 0.158 the wait timed out instead of reporting blocked. The matchers now accept the old wording or the new row, tolerating the spaces the line-folded text copy drops around `·`. Each one matches from the dialog's first `·` (for the update dialog that is its title row, `Update available · 0.157.0 → …`), so the dialog is blocked from the same character that would otherwise make the provisional header read as live. The retired-model notice without choices has a catalog-supplied heading (`GPT-5.4 is no longer available`), so it is matched by its own key row. No new blocked-reason value. The startup-dialog matchers move to startup-dialog-blocked-signals.ts to keep terminal-wait-detection.ts under the line limit. Backed by six real captures (update available, hooks review, retired model without choices, each on 0.157.0 and 0.158.0), replayed frame by frame and through a tui-idle wait; the serializer round-trip replay registers their existing live-pen colour divergences. * fix(runtime): keep reporting Codex's retired-model notice after a relaunch in the same pane The retired-model notice is matched by its key row alone, and the matcher took the first `enter/esc continue ·` in the live window while every other startup-dialog matcher takes the last. Quitting Codex from the notice and relaunching it in the same pane leaves the old copy ahead of the new launch's header, so the header read as having dismissed the new notice: 0.157 then read ready and a worker brief would be typed into the dialog. Take the last key row, and replay each captured dialog quit-and-relaunched to pin all six. * fix(runtime): match Codex startup dialogs by the rows the text copy keeps intact Codex 0.157+ paints each startup dialog over its startup screen by cell diff, so Orca's line-folded text copy can drop letters and spaces from a heading: #23765's 0.157.1 capture reads `Updat available`. The update matcher needed `update available`, so on that capture tier 1 read the dialog's own `·` as the live chat's footer and a tui-idle wait settled ready on the update dialog, whose Enter picks "Update now". Match each dialog from its first `·` by rows Codex prints as fixed literals: `available · <version>` and `enter continue · esc skip` (update), `enter confirm ·` (hooks review), `enter/esc continue|confirm ·` (model notices, which also covers 0.158's new-model announcement, so its choice-text matcher goes). Legacy `Press enter to …` wording still matches. Add the new rows to the blocked-signal prefilter, and replay #23765's 0.157.1 update capture in the dialog suite. * fix(runtime): don't name Codex's mid-session pickers a hooks review Codex's rate-limit reset popup (and its other pickers) end their key row with `enter confirm · esc back`, which the hooks-review row matched now that it no longer needs the heading. Exclude `esc back` instead of requiring `esc skip`, so a half-painted hooks-review row still blocks. |
||
|
|
bb91e39eb9 |
test(native-chat): main's Codex child tests expect a turn to end on its own turn/completed (#23783)
* fix(native-chat): a Codex child's turn ends on its own turn/completed #22553 ended a child thread's turn on an `error` Codex will not retry, reading the verdict module #23682 deleted when it made turn/completed the only end of a Codex turn. The two landed minutes apart with no textual conflict, so main no longer typechecks. Codex runs every thread's events, spawned children included, through the same per-thread handler: a turn-ending error is recorded as the turn's last error and the turn then completes as failed. So a child's failed turn/completed is its end, as on the primary thread, and a closed thread stays the one child ending with no completion. * refactor(native-chat): a closed Codex child thread is its own frame With a child's turn now ending only on its own turn/completed, the turn-ended frame carried a fixed turnId (null) and state (unverifiable), and endTurn took parameters nothing passed. Name the one remaining case: a thread-closed frame, and closeThread ending the running turn as unverifiable. Drop a test step that no longer exercised anything. |
||
|
|
b6c2de9f92 |
fix(codex): index a new account home before bridging history into it (#22971)
* fix(codex): index a new account home before bridging history into it Codex indexes every rollout present when it first creates its state DB, and the TUI gives up after 30s, so large histories broke a new account's launch. Fixes #20669 * test(codex): keep the account migration test from starting the real Codex binary Selecting an account starts the history bridge, which spawned codex app-server on the fixture homes and raced the test's temp-dir cleanup. * fix(codex): index a new account's most recent bridged history first Indexing a large history takes minutes, and Codex's /resume hides unindexed threads once a directory has any indexed one, so recent conversations stayed missing until the heal reached them in directory-listing order. * test(codex): cover the history bridge's quit, no-history and failed-link guards Drop the stop check before creating the state DB: it ran in the same tick as the check at bridge start, so it could never observe a quit. * test(codex): make the account heal tests fail closed instead of reaching a real codex Fake invocations now point at a nonexistent binary and a throwaway CODEX_HOME, and cover per-home failure memory and a session that fails during quit. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
bd133058a9 |
refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip (#22565)
* test(sidebar): pin today's subagent rows and chat strip rows from legacy shapes Captured on the unmodified renderer: the compact and full sidebar child rows built from a legacy subagents snapshot, and the expanded chat strip built from a legacy background-task roster. A host that sends only these shapes must keep rendering exactly these rows. * refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip A child row's dot, name and detail are now decided once, by a shared row model (src/shared/agent-child-row-model.ts), and rendered by one piece (AgentChildRowContent) that both the sidebar's child rows and the chat strip use. The model reads the host's child views when a session publishes them and today's legacy shapes otherwise (the subagents snapshot for the sidebar, the task roster for the strip), so old hosts and CLI panes render exactly as before. From views it keeps what the legacy path lost: a finished subagent whose shell still runs reads monitoring through the shared child fold, a settled child reads by its outcome, the tool it runs shows as the CLI row shows it, and each row keeps its own clock. In the strip, work a child owns renders nested beneath it. * i18n: add the child row's Ended and ended-ago strings to every catalog * test(sidebar): a child reads the same in the sidebar and the chat strip A row-model table for every display state, and a parity table that renders the same child views through the compact sidebar row and the chat strip and asserts both show the same dot, name and detail. Covers a finished subagent whose shell still runs (monitoring on both, no stale tool text), sibling rows with their own clocks, a settled row timed from when it ended, and owned shells nested under their owner in the strip. The strip's view input is named childViews so it cannot be mistaken for React children. * fix(sidebar): keep the child display derivation loadable in the renderer The row model imported deriveAgentChildDisplayState from the view module, whose owner resolution reaches status subjects and, through them, agent-hook-relay's node:crypto. The renderer cannot evaluate that chunk, so the app booted blank (the renderer node-builtin boundary test fails on the previous commit). The display derivation (agentChildWorkOwnedLiveness, deriveAgentChildDisplayState, AgentChildDisplayState) now lives in agent-status-child-work-display.ts, which imports only the fold, liveness and the one-pass grouping both modules share (grouped-by.ts); the view module keeps the host-side projection. * fix(sidebar): the strip reads its parent's verdict, and the full row says Ended Review fixes: - The strip's view path took no freshness input, so once views are wired the same lost child would read unverifiable in the sidebar and working in the strip. agentChildRowContextForParent builds the one context a parent row gives its children; the sidebar uses it, and buildBackgroundTaskGroupsFromViews / the strip's new optional childRowContext prop accept it (absent: claims stand as reported, as before). - The full sidebar row showed no word for a child that ended with no outcome; its message line now reads the row model's (the message, or Ended). An unlabeled child falls back to its display state there too. - The summary order now includes failed, so a failed row is never dropped from the counts. - Parity now covers the full row for every state, a live parked child, an unlabeled child, and a lost or stale parent on both surfaces. * refactor(agent-status): the subagent snapshot, its normalization and equality get their own module agent-status-types.ts crossed the file-size limit once the row gained child views beside the main agent fact. The subagent snapshot shape, its admission normalization and the array equality move into agent-status-subagent-snapshot.ts (re-exported, so importers are unchanged); the three field caps it shares with the row move to the field normalization. * refactor(sidebar): one legacy builder family, one reader clock, frozen settled rows - The chat strip's task-roster rows are built in the shared row model beside the other two builders, with one placeholder set and the one detail rule. The module header states when each legacy builder is deleted. - A child's "No update" duration reads the parent's reader clock (receipt time for a mirrored parent); a view child's own stamp is moved onto that clock. The full sidebar row reads the same value as the compact row. - The failed->blocked / interrupted->idle collapse has one owner, shared by the sidebar row state and the strip header. - A settled strip row shows its run frozen at settledAt instead of a growing "ended N ago", so a strip of only finished work never wakes the 1 Hz tick. * test(sidebar): the sidebar row state and the strip header share one lifecycle word * test(sidebar): a child running a shell in its turn shows its Bash line with the shell nested beneath it While a child's turn runs its shell, the host records the shell both as the child's operation and as a live command the child owns. The strip shows the child working its Bash line with the shell row nested beneath it, the header counts only the agent, and the sidebar shows the child alone with the same text. * test(sidebar): re-pin the legacy chat strip golden to main's scoped goal-dock selector Main's #23725 rewrote the strip's goal-dock variants from `group-has-[...]/tasks:` to an ancestor-scoped `[[data-native-chat-background-tasks]:has(+[data-native-chat-thread-goal])_&]:` selector. The merged head renders byte-identically to main for this fixture; only the golden was captured before that change. |
||
|
|
357a2fed08 |
fix(native-chat): group chat rows by the turn that produced them (#23671)
* fix(native-chat): keep a turn's bar on the prompt that opened it A message sent while a structured turn runs appears in the transcript at once, so "the newest user message" is not the running turn's owner. The live "Working for" bar moved to the mid-turn message and counted from the earlier prompt's start, and a send queued behind the running turn counted its wait twice: once in the previous turn and again from its own send. Derive both from the host's turn records in one ordered pass: - The running turn's bar belongs to the user message its lifecycle row names (resolved exactly as settled timing resolves it). A message sent mid-turn gets no bar until its own turn opens; a send folded into the running turn never gets one. Surfaces fall back to the latest user message only when the host names no opener. - A turn counts from its send, but never before the previous turn in the journal ended (its recorded end, else its row's last host revision), capped at the turn's own start. The same origin feeds the live counter and the settled duration. Desktop and mobile share the derivation; no wire, host, or storage change. * feat(native-chat): derive each transcript row's owning turn from the journal Rows between a turn record and the next belong to that record's turn, so a message the provider folds into a running turn no longer captures the rows produced after it. A turn whose opener the host cannot name in the loaded window anchors to its own record instead of a bystander prompt, and shared nativeChatRowTurnKeys keeps positional preceding-user grouping for anything the host does not attribute (older hosts stay pixel-identical). * fix(native-chat): fold and time transcript rows by their owning turn on desktop A settled turn's bar now folds every row the turn produced, including rows after a mid-turn send; the steered bubble stays visible and carries no bar. A provider-opened turn renders its bar above its first row instead of borrowing the newest prompt, and row liveness follows the owning turn rather than the newest user message, so a running turn's rows stay live while a send waits. * fix(mobile): group phone transcript rows and bars by their owning turn Same shared derivation as desktop: the opener's bar owns every row of its turn across a mid-turn send, a steered bubble never grows a bar, a provider-opened turn's bar sits above its first row, and a running turn's tool rows stay live while a newer message waits behind it. * fix(mobile): declare the turn ownership map on the chat controller contract * fix(native-chat): one diff rollup per turn, and no wake-turn clock on a later prompt A turn's rows are no longer contiguous once rows are grouped by owning turn: a prompt sent before the running turn's last row lands among its rows. The diff rollup was drawn at every run boundary, so such a turn showed its rollup twice and the later prompt's rollup appeared under its bubble. It now renders once, under the turn's last row. On the phone, a turn keyed to its own record is not a user message, so when it ended its clock was treated as a replaced optimistic echo and handed to the newest prompt - a message sent during a wake turn got a bar with the wake turn's duration. Host-attributed turn keys now count as live turn keys. * fix(native-chat): keep a Codex turn's bar on its send until the echo lands Codex reports turn/started before it runs hooks and prewarm and before it echoes the send, so for that gap the turn names a provider key no alias resolves yet. Anchoring it to its own record left the running turn with no bar at all; treat the send still in flight ahead of the record as its opener. * fix(codex): restore each turn's record ahead of that turn's items Rows are grouped by the nearest turn record before them in journal order, which holds on the live path because a turn's record is written when the turn opens. Full-history restore (the fallback for Codex app-servers that reject excludeTurns) wrote each turn's items first and its record after, so every restored turn's rows were credited to the previous turn and turn 1's answer folded away. The restore now appends the record before the turn's items. The record itself is unchanged: same identity, state, outcome, opener key, endpoints and duration, one append each. The restore-order test now expects the record first, because that order is what keeps grouping correct; its old order was incidental, not a contract any reader relied on. Readers that key turns by id or opener key, or that scan for the newest record (all restored records are settled), read the same result in either order. Journals already imported in the old order stay as written; no migration. |
||
|
|
b7e3bbf3d1 |
fix(browser): restore hover after leaving a mobile viewport preset (#22846)
Turning touch emulation off sent maxTouchPoints: 0, which Chromium rejects (it only accepts 1-16, even when disabling). Touch emulation stayed on, so pages kept reporting no hover and a coarse pointer, and the desktop user agent was never restored. Omit maxTouchPoints when disabling so Chromium restores the original value. |
||
|
|
b8a7dafafc | fix(codex): guard the daemon socket in the home a resumed Codex pane launches in (#23724) | ||
|
|
f4829f0363 |
fix(renderer): stop the modal toast rule from freezing large diffs (#23721)
* fix(renderer): stop the modal toast rule from freezing large diffs The rule that lifts toasts above a dialog or sheet backdrop matched body:has(<overlay> anywhere). Chromium re-evaluates a descendant :has() on body for DOM changes anywhere in the page, so with large Monaco diffs open every editor mutation re-scanned the document and the renderer stopped responding. Dialog and sheet overlays portal straight into body, so matching them as direct children keeps the same behaviour while only changes to body's own children can affect the rule. * fix(renderer): raise modal toasts through a body variable, not a descendant :has() The child-combinator form still ran a whole-page walk after unrelated DOM changes (about 3x cheaper than the original, but still proportional to page size). Setting a custom property from a :has() on body alone takes the rule off that path; only opening or closing a backdrop restyles. * test(renderer): fail the toaster layering guard when the child combinator is dropped The guard only rejected a :has() inside the toaster's selector. Dropping the `>` from the body :has() (or removing the lift rule) still passed, and the unscoped form re-runs on every data-slot element change anywhere in the page, the same freeze the PR fixes. Assert the exact child-only rule shape. |
||
|
|
33f661b745 |
fix(native-chat): an empty workspace opens the default agent as a chat when chat is the default view (#23693)
* fix(native-chat): an empty workspace opens the default agent as a chat when chat is the default view With "open agent tabs in chat" on, clicking a workspace with no tabs still seeded a bare shell. The seeding path now opens the user's default agent through the shared launch funnel when that launch routes to a chat, and keeps the shell otherwise. A Blank Terminal pick at create time is now passed to activation as agent: null so it still gets a shell. * fix(native-chat): only a deliberate workspace open starts the default agent chat Round-1 review of the empty-workspace default chat: activation treated any call with no agent and no caller surface as user navigation, so CLI/phone creates, fallbacks after a failed agent launch, fork fallbacks, and the move to a neighbour after a delete all opened an agent chat nobody picked. - Opt in instead: activation options gain navigationIntent: 'user-open', set only by user navigation (sidebar row, keyboard cycling, Cmd+J, the workspace digit shortcut, back/forward, open-parent, jump-to-workspace, and the open-attached-workspace / space-manager actions). Every other activation keeps today's shell, so the Blank Terminal pass-through in the create flow is no longer needed and is reverted. - The Electron gated reseed now waits (bounded, 5 s) for the workspace host's agent list when it has not loaded, holding a per-workspace claim that the passive seed and other reseeds respect, then re-checks the active workspace, host, and emptiness. Host resolution reuses the detection target key, so an unresolved owner stays unknown. - Tests cover the gated path for worktrees and folders, non-opted activations, history navigation, the detection wait and its failure, and the passive seed deferring to the claim. * fix(native-chat): a reopen during the agent-list wait seeds for the latest open Round-2 review of the empty-workspace default chat: while a user open waited for the host's agent list, a second activation of the same workspace was dropped. Opening A from the sidebar, moving to B, then pressing Back to A during the wait left A active with no chat and no shell: the wait reseeded with the first open's host id, which no longer matched, and the passive seed had already marked A handled. - The pending wait now stores the latest activation's intent; a later activation (user open or plain) replaces it, and the wait seeds with it. - The passive seed no longer marks a workspace handled while a wait owns it, so leaving mid-wait and returning seeds a shell instead of nothing. - The wait and the open share one check, so a Blank Terminal default (or chat not being the default view) no longer waits up to 5 s before its shell. |
||
|
|
25d9c57e3a |
fix(native-chat): keep a child turn settling on an error Codex will not retry (#23808)
#23801 removed the child-path reading of Codex's turn-ending `error` along with the import of the module #23682 deleted. That was the wrong half to remove: the primary journal path can rely on Codex's failed `turn/completed` arriving within ~32 ms, which is what #23682 established, but a child turn has no such guarantee, and without the error as its end the child's lifecycle row latches on `working` for the life of the session. Three tests assert exactly that and could not run, because the unresolved import had been skipping the unit matrix since #23682 merged. The reading is restored inline against `readCodexErrorWillRetry`, itself restored to `codex-structured-thread-facts.ts`, rather than by reviving the deleted module: its `thread-stopped-running` arm lost its only consumer when #23682 rewrote the primary path, so restoring the file would re-add dead code. Also drops `pr-workflow-parallelism.test.mjs`'s read of `.github/workflows/track-community-prs.yaml`, which #23796 deleted while leaving the assertion behind. Same failure class, and it fails the same shard. |
||
|
|
2d358df8af |
fix(i18n): prune the wait-for-setup help copy #23799 removed (#23802)
#23799 removed both `waitForSetupBeforeAgentHelp` call sites but left the entry in all six locale catalogs. The runtime-required generator treats an entry with no literal-default call site as one only the catalog can serve, so the two dead entries had to ship in the boot bundle for the catalog check to pass, and `verify:localization-catalogs` failed on every pull request until they did. Deleting them is the resolution rather than regenerating `en-runtime-required.json`: no call site can reach either string, so shipping them would add dead weight to the boot bundle to satisfy a check about what the bundle must contain. The sibling `waitForSetupBeforeAgent` heading keys stay; #23799 removed only the help paragraphs. |
||
|
|
643f93f009 |
fix(native-chat): drop the Codex error path #23682 removed (#23801)
#23682 established that only `turn/completed` ends a Codex turn and deleted `codex-structured-journal-provider-verdicts.ts`, but the child-turn reader kept importing `readCodexProviderVerdict` and branching on its `turn-failed` verdict. The module is gone, so the import resolves to nothing: typecheck and static analysis fail on every pull request, which skips the whole unit matrix behind them, and the esbuild pass behind Bun profile scope detection throws, so every run falls back to all six platforms. A closed thread is now the only child-turn end without `turn/completed`, which is what #23682 intended: Codex follows a turn-ending `error` with a failed `turn/completed` for the same turn 0-32 ms later, and that completion carries the duration and receipt time the error does not. |
||
|
|
ceffecf12a | fix(setup): remove wait-for-setup helper text (#23799) | ||
|
|
7445feaca8 |
fix(terminal): only diagnose disk exhaustion from capacity errors
Merged after required checks passed. |
||
|
|
18bd9cb1f6 | test(status-bar): cover re-probing a roomier level after the tightest level's fit moves (#23793) | ||
|
|
c8af48d8a4 |
fix(runtime): settle a quiet Codex composer as ready on every version (#23765)
* fix(runtime): settle a quiet Codex composer as tui-idle on every version Codex 0.158 dropped `model:`/`directory:` from its startup header, which both Codex readiness rules require, so `worker-start --agent codex` timed out; an idle Codex pane after a turn also had no readiness signal once the header left the screen. Generalize the Muse tier-1b lane into a quiet-ready-screen lane: a Codex (or agent-unknown) pane whose live screen shows the empty composer placeholder, no `to interrupt)` status row, no header `loading`, and no dialog wording in its live window, settles once the stream has been quiet for the tui-idle quiescence window. Additive only: the tier-1 rules and the Muse rule are unchanged. Fixtures: codex 0.150.1-0.158.0 captures at 120x40, including chunk-timed turns. * refactor(runtime): anchor the Codex quiet lane to the empty composer line Move the Codex screen rules into codex-terminal-readiness.ts and the quiet-screen body beside isKnownReadyPromptBody. The composer rule now matches only the `› Ask Codex to do anything` line and drops its dialog markers: every Codex dialog replaces the composer, and an answer ending "Would you like to…?" above a live composer must not hold the lane forever. The quiet lane checks quiescence before reading the screen. Trim the redundant startup and untimed turn fixtures. * fix(runtime): read Codex's busy row above the composer and scope the lane to codex panes * fix(runtime): read only Codex's live status row above the composer |
||
|
|
c30c8f9d77 |
fix(native-chat): a message Claude folds into its running turn no longer splits the turn (#23621)
* fix(native-chat): treat a folded mid-turn Claude replay as a receipt, not a turn boundary A message sent while a Claude turn runs is folded into the running turn by the CLI and replayed mid-turn with the client uuid. The replay-driven opener treated that replay as a new turn: the running turn was marked interrupted and the 'Worked for' bar split. The dispatch waiter now captures the open turn at write time (sentDuringTurnId, volatile); a replay that adopts the client uuid while that exact turn is still open settles delivery and opens no boundary. Plural result user_message_uuids settle each waiter under its own uuid; every other relation (miss, replaced turn, provider-resumed root, idle write, fresh replay uuid) keeps the opener path. Replay/turn resolution split out of the dispatch module to hold the line budget. * fix(native-chat): state the fold receipt's uuid-adoption limit as unmeasured The receipt admits only a replay that adopts the client uuid. The comment and test named a fresh replay uuid as "the CLI starting the queued send's own turn", but the measured miss case adopts the client uuid too, so adoption does not tell a fold from a later turn. Say what the rule actually is: the measured fold shape qualifies, and an unmeasured fresh-uuid replay keeps the opener path it always had. No behavior change. * fix(native-chat): decide Claude fold receipts from the provider's own request cycle Measured (p3 captures, CLI 2.1.280): the CLI folds any send that arrives while its request cycle runs — including one written before the first replay — and it announces every new cycle (sequential turn, queued turn, background wake, /compact) with a root system/init; each result names the sends its cycle ran in user_message_uuids. So the fold decision now reads provider state: an adopted replay while the open turn's cycle is still live (no root init since it opened) is a delivery receipt. The send-time bookkeeping (sentDuringTurnId) is removed; it missed the measured early-steer fold and guessed at what the provider states outright. A lost result is covered by the init staleness mark, and CLIs below the per-turn-init version floor (or with no reported version) keep today's opener path via an explicit gate on the init frame's claude_code_version. * fix(native-chat): read Claude fold membership from the cycle's own work A finished background task revises its row before the wake cycle's init, so the wake turn opened ahead of that init and was marked stale by it: a send folded into the wake still split the bar and interrupted the wake turn (captured p3-background-wake order). A send replayed after the current cycle's first root work (send echo or model output) is the fold; the cycle's first send is its opener. Init and every settle start a cycle with no work. * fix(native-chat): adopt the CLI version from any init frame, not only the startup proof Live sessions prove the session from a SessionStart hook frame that arrives BEFORE system/init, so startup facts read a frame with no claude_code_version and the fold receipt's version gate never passed: the real app still split the bar and marked the running turn interrupted while every fixture test — whose harness proves startup with the init frame itself — stayed green. The version is now adopted from whichever init frame carries it, at the same site that already adopts the per-turn model report, and the startup proof never clobbers a version a real init already reported. Pinned twice: a fixture run whose startup proof is the SessionStart hook frame, and a real-CLI fold test (skipped without a signed-in CLI) that fails on the unfixed branch at the interrupted assertion and passes with the fix. * test(native-chat): run the real-CLI fold test in the config dir it probed The connection strips an inherited CLAUDE_CONFIG_DIR and inherited auth vars, and with no launch env the pin compared against that same inherited value and emitted nothing, so the fold test always ran against ~/.claude whatever CLAUDE_CONFIG_DIR the availability probe checked. It also relied on the user's own SessionStart hooks for the live proof order and on their permission rules for the Bash steps; an isolated home hung at startup. The test now launches with the probed home and env auth, and pins a SessionStart hook and the sleep permission itself. * fix(native-chat): drop the CLI-version gate and prove the fold against full adapter captures The fold rule stands on frame-derived facts alone: an adopted client uuid (the capability check, read off the replay itself) while the open turn's cycle has done root work. The claude_code_version floor guarded only an unobserved triple fault — an adopting CLI without per-turn init AND a lost result — and its cost was a silent latch that already fired once live; all cliVersion plumbing is removed. Mid-turn auto-compaction was measured (forced via CLAUDE_CODE_AUTO_COMPACT_WINDOW): it emits status/compact_boundary and a synthetic continuation but NO root init, so the init cycle reset stays and the capture is pinned. The fake connection now defaults to the live startup shape (SessionStart hook proof; one init when the first command starts a cycle; capabilities on the initialize result), with init-at-startup an explicit unmeasured opt-in. Full frame streams recorded through Orca's own adapter against the real CLI are committed and replayed verbatim, asserting the provider's membership fact (each result's user_message_uuids) rather than design internals. * test(native-chat): drop the removed CLI-version gate from fold test comments Also reattaches the fake harness's initProof doc to initProof (it had landed on contextUsage, replacing that field's own doc) and renames a plural-result test whose title described a retired waiter it never creates. * test(native-chat): name the fold test's settings for their role * chore: take main's lockfile back after the merge * test(claude): route the real-CLI probe through the spawn chokepoint; give the slow-init test a live startup report The shared real-CLI availability probe moved out of a test file, so the child_process and CLI-runtime-pairing ratchets now scan it. It spawns through runProcessSync with the CLI paired to its own node, as the structured launch does. The provider-started test's CLI default now reaches startup the live way: get_settings reports it, since system/init only arrives with the first command. |
||
|
|
50a8ef18e4 |
fix(native-chat): keep a turn's bar on the prompt that opened it (#23573)
A message sent while a structured turn runs appears in the transcript at once, so "the newest user message" is not the running turn's owner. The live "Working for" bar moved to the mid-turn message and counted from the earlier prompt's start, and a send queued behind the running turn counted its wait twice: once in the previous turn and again from its own send. Derive both from the host's turn records in one ordered pass: - The running turn's bar belongs to the user message its lifecycle row names (resolved exactly as settled timing resolves it). A message sent mid-turn gets no bar until its own turn opens; a send folded into the running turn never gets one. Surfaces fall back to the latest user message only when the host names no opener. - A turn counts from its send, but never before the previous turn in the journal ended (its recorded end, else its row's last host revision), capped at the turn's own start. The same origin feeds the live counter and the settled duration. Desktop and mobile share the derivation; no wire, host, or storage change. |
||
|
|
e899809ff8 |
fix(mobile): unsubscribe session tabs by request on the direct connection (#22943)
* fix(mobile): unsubscribe session tabs by request on the direct connection * fix(mobile): hold a direct session tabs unsubscribe until the first snapshot The desktop registers a tab-list stream only as it emits the first snapshot. A direct unsubscribe sent before that found nothing, and with per-request addressing no later worktree-wide sweep collects the late stream, so it kept its desktop listener until the socket closed. Hold the unsubscribe until the snapshot arrives, as the relay connection does. * test(mobile): cover a held session tabs unsubscribe whose subscribe fails * fix(mobile): keep the session tabs hold within the registry line budget after merging main Move the pre-snapshot hold into the session tabs stream module, note that only older hosts need it, and give the unsubscribe test the real registration version now that a worktree-wide unsubscribe spares later streams. |
||
|
|
2ae7c00e84 |
fix(opencode): keep OpenCode 2 panes Working across plugin reloads (#23700)
* fix(opencode): keep OpenCode 2 panes Working across plugin reloads OpenCode 2 disposes and re-sets-up every plugin whenever its plugins dir changes, while sessions keep running. The status plugin published a final Idle on dispose, so a pane read Done mid-turn. Orca also rewrote the plugin file on every PTY spawn, so opening any terminal triggered that reload. Dispose now releases the factory's bookkeeping without publishing a verdict; the next lifecycle event settles the pane, and Orca's ended-process reconciliation still retires panes whose agent exited. The plugin file is written only when its bytes differ. * fix(opencode): skip rewriting an unchanged plugin in the SSH relay install too The relay's canonical-config install still unlinked and rewrote the status plugin on every OpenCode launch over SSH, which restarts every plugin in a remote OpenCode 2 server. Share one install-currency check (lstat + the existing byte comparison) between the local and relay writers, and pin write-if-changed with mtime so the tests also fail on filesystems that reuse a freed inode. * fix(opencode): keep the final Idle when OpenCode 1 tears its instance down OpenCode 1 disposes a plugin only when it tears the instance down, and that teardown cancels every running session, so the Idle published on dispose is true there; the cancelled run's own idle may never reach the plugin. Only OpenCode 2 disposes on a hot reload while turns keep running. The generated module serves both hosts, so the OpenCode 2 setup() entry point now tells the shared factory that sessions outlive disposal; the server() path keeps the previous disposal behaviour, including the hand-off to a surviving factory. * fix(opencode): compare a symlinked plugin by its target before rewriting OpenCode 2 loads plugins through file-level symlinks and reads the revision from the target's mtime, so a user whose Orca plugin file is a symlink (per-file dotfile managers) failed the regular-file check and got a write through the link, and a reload, on every spawn. The config-dir and relay installs now skip the write when the resolved target already has Orca's bytes; when stale they behave as before. Only the per-source overlay keeps the regular-file check, since a link there mirrors a user entry. Installers also skip the write inside a guarded block rather than returning early, so later install steps still run. * test(opencode): skip the plugin symlink tests on Windows like their neighbours Creating a file symlink on Windows needs Developer Mode or admin rights. * test(opencode): stub fetch without a type assertion in the dispose host test |
||
|
|
3976ad4c59 | perf(test): remove obsolete structural snapshots (#23777) | ||
|
|
b776e9ac99 |
fix(browser): give a tab's identity one owner so viewport presets stop dropping client hints (#23718)
* fix(browser): give a tab's identity one owner so viewport presets stop dropping client hints A desktop viewport preset installed a CDP user-agent override with no userAgentMetadata. Chromium then drops navigator.userAgentData and every sec-ch-ua header for that tab: a Chrome UA with no client hints. Identity was decided separately by the session request hook, the Google sign-in switch and the viewport code, and nothing decided per tab who it should claim to be. resolveBrowserTabIdentity now derives it from the process identity mode, whether the URL is a Google auth host, and whether a mobile preset is requested. applyTabIdentity is the one writer: it keeps the WebContents UA on the process or Firefox identity and clears the CDP override whenever that layer already presents the identity. Viewport emulation only records the requested preset; its metrics and touch steps log failures independently, so a rejected step can no longer skip the identity restore. * test(browser): read the presented identity instead of casting the guest stub * test(browser): drop a comment that described desktop presets writing a UA * fix(browser): keep same-document navigations and unapplied presets off the tab identity A same-document navigation (pushState/replaceState) now never rewrites the WebContents user agent. Chromium reloads a still-loading document when its user agent changes, so an OAuth callback that strips its code with replaceState after a redirect off Google sign-in was requested twice, replaying the one-time code. Measured on Electron 43.7.5: the callback URL hits the server twice with the write, once without. The session request hook now derives the mobile identity from the CDP override the tab actually holds instead of the requested preset. A preset whose write never landed (debugger attach refused while DevTools is open, a failed write, a detach) no longer puts the iPhone user agent and mobile client hints on the wire while the document reports desktop. * fix(browser): restore identity after a failed navigation without reloading the error page did-fail-load fires while the failed URL's error page is still loading, and WebContents.setUserAgent() at that moment makes Chromium reload it. After a redirect onto or off the Google sign-in host (identity moved over CDP only), the restore rewrote the WebContents UA there and replayed the failed request. The restore now goes over CDP; the next navigation rewrites the WebContents UA. * refactor(browser): let only a navigation start write the WebContents user agent Two review rounds each found a caller that asked the identity writer to rewrite the WebContents UA at a moment Chromium reloads or cancels the page (a same-document navigation, a failed load). A boolean at every call site left that decision to the callers. The writer now has two entry points: presentTabIdentityAtNavigationStart, the only one that may write the WebContents UA and only for a cross-document navigation, and retargetTabIdentity, which goes over CDP only and serves redirects, failed loads and preset changes. A table test pins the rule for every entry point. * test(browser): reject touch emulation regardless of payload in the identity-restore test The mock rejected only maxTouchPoints 0, so the test would stop exercising a failed touch step once the touch payload is fixed. |
||
|
|
b4c19f12c4 |
fix(claude): run structured Claude under the POSIX provider supervisor (#23476)
* fix(codex): the provider supervisor outlives its provider group when stopped A signalled supervisor forwards the signal to the provider group, escalates to SIGKILL after the grace, and exits only once the group is gone, so recovery's proof that the recorded pid is dead also proves the provider is. It refuses to spawn when its parent is already not the owner named in its spec, and watches that owner rather than whichever parent it first saw. The grace is a spec field. Recovery's SIGTERM stage now outlasts the supervisor's own stop, since a SIGKILL that lands first cannot be handled and leaves the group running. * fix(codex): a closed owner pipe no longer ends the supervisor before its provider group When Orca dies, the supervisor's stdout pipe has no reader. Provider output in the window before the parent-death watch fired raised an unhandled EPIPE that exited the supervisor with the provider group still running. * fix(codex): bound the supervisor grace so recovery's SIGTERM stage always covers it Recovery sized its SIGTERM stage from the default grace, so a launch with a longer grace would be SIGKILLed mid-stop and orphan its group with no test noticing. The spec now refuses any grace above one exported maximum, and recovery derives its SIGTERM stage from that maximum. * fix(codex): every supervisor stop asks the provider with SIGTERM first Owner death, stdin end after the grace, and a signal to the supervisor now all take one path: SIGTERM the provider group, SIGKILL it after the grace, and exit only once it is gone. The signal handlers are registered before the provider is spawned, so a stop that lands in the spawn window still reaps it. The longest stop grows to two graces plus the reap wait, and both recovery's SIGTERM stage and the connection's graceful close now wait that long before forcing, since forcing the supervisor sooner can orphan its group. * fix(claude): run the structured Claude child under the POSIX provider supervisor A close now stops Claude with a SIGTERM through the supervisor instead of letting stdin end finish the turn, and Orca's death stops it through the supervisor. * test(claude): pin the supervised stop against a real Claude CLI, opt-in * test(claude): a requested stop reads interrupted through the frames the supervised SIGTERM makes Claude emit * test(claude): show what the real CLI did when it never ran the tool * test(claude): Orca's death now reaps Claude's own tool through its SIGTERM * refactor(claude): take supervision from the spawn spec so the close ladder cannot disagree with the spawn createProviderSpawnSpec now reports whether it wrapped the provider in the supervisor, and the Claude spawner reads that instead of repeating the platform check. The close ladder's SIGTERM follows the process actually spawned. * fix(native-chat): derive quit's chat-eviction bound from the longest supervised provider close Quit's child-eviction phase was a hand-picked 8 s. It is now the sink drain plus the longest supervised close over Claude and Codex plus a named 1 s margin, so a provider close that grows widens it instead of silently outrunning it. A close's tree-kill fallback stays outside the bound: once main exits, the supervisor stops its provider group on owner death, which a new test now proves for a clean owner exit, and next launch's recovery settles the lease. |
||
|
|
2dc2693953 |
fix(native-chat): a turn a proven crash cut short reads interrupted (#23456)
* fix(native-chat): a turn a proven crash cut short reads interrupted, ending when it was last seen working * fix(native-chat): end a probe-proven turn at the last row the journal wrote live A revised item keeps its first sighting's timestamp, so a long command or a streamed reply read as ending when it started. The reducer now tracks the latest live row the same way it tracks all activity. * test(native-chat): give the unexpected-exit fake journal its live-activity read * refactor(native-chat): read the journal's live bound only for a probe-proven death * fix(native-chat): mark what a journal open settles for a gone host as crash reconciliation A crashed host's working roster is retired when the journal reopens. That row was written live, so a probe-proven turn ended at the relaunch and counted the downtime. * fix(native-chat): bound a probe-proven death with the last time Orca proved the owner alive A crash mid-tool left the turn ending at the tool call's start, because Claude writes nothing while a Bash call runs. The death evidence now records the lease's last renewal before the death as lastProvenAliveAt, and the turn ends at the later of that and the last live row, capped at the probe. Parking a lease in recovery no longer stamps lastRenewedAt, since nothing proved the owner alive then; a child that outlived Orca would otherwise have its turn count the downtime. * test(native-chat): a failed acquisition parked in recovery keeps its last proof of life, and the timing read goes through the display selector * refactor(native-chat): move the submission dispatch folds out of the journal reducer Main grew the reducer to its line limit, so the live-activity bound tipped it over. The dispatch row and echo-acceptance folds move unchanged into their own module. * test(native-chat): a send or reader that opens a crashed chat settles a proven death interrupted Main's open-time settle test still asserted the old rule, where only a watched exit proved a death. * docs(native-chat): say which proofs of death record a last proof of life * fix(native-chat): a proof of life bounds only the owner that wrote the turn A start after a crash that spawned a child and then failed without proving it gone parks that child for recovery; when recovery finds it gone, the proof of death carries its proof of life, which is after the crash. The older turn then ended there and counted the downtime. The journal now derives the fence of its newest live writer, and the lease that holds the proof names the owner it released by the fence it moved to. The last renewal counts only when that move was one step past the writer of the turn. * test(native-chat): a crash with a send in doubt still ends at the last proof of life The reopen settles that send at the new fence, so the owner check must read the fence of live rows only. * fix(native-chat): a proof of death judges only the turn its own owner wrote The settle read the record's latest proof of death for whatever turn a gone generation left running. After a crash, a start that reserved a new fence cleared the relaunch's proof, and if it then failed, its own child's death (a watched exit at the failure, or a probe finding the child it left for recovery gone) ended the older turn an hour after the crash. Every proof of death now records ownerFence, the fence of the owner or reservation it is about; a fence names exactly one owner. The journal derives the fence each item was created at, and a running turn is interrupted only by a proof naming its own owner; otherwise it is unverifiable. Evidence older builds wrote keeps their rule. This replaces the derived one-step fence check. * test(native-chat): every writer of a watched exit names the owner it released A watched exit that names no owner reads the older rule, so the settle alone cannot tell a dropped field; the writers are pinned directly, including past a recovery floor. * test(native-chat): give the fake journal's cast its safety rationale * fix(native-chat): a proof of death written after a chat opened revises the turn it left unverifiable On desktop the chat on screen at relaunch opens before the startup reconcile has probed its owner, so the open settles the cut-off turn unverifiable. When the reconcile then records the proof, it re-runs the same settle for every open conversation, which revises that owner's unverifiable turns to interrupted with the proof's end. Any later open re-runs it too, so a failed write converges. Only upward, only for a proof that names the turn's own owner. * test(native-chat): a chat read before the reconcile reads unverifiable, then interrupted Covers the reconcile revising an open chat to the last renewal (27 s) and a subscriber being sent both states, a start after the crash whose running turn the queued revision leaves alone, a failed revision write converging at the next open, a proof about another owner or from an older build never revising, and a second settle writing nothing. * fix(native-chat): revise an open chat's turn wherever a proof of death is written The store tells its listeners, once committed, of each record a transaction gave a new proof of death, so every writer (the startup reconcile, a recovery that stopped a child which outlived Orca, a failed start, a watched exit) triggers the same serialized resettle for a chat already open. The reconcile's own callback is gone. Quit stops listening first, and a queued resettle is drained with the starts. * test(native-chat): a failed exit settlement is retried in place once the exit is recorded Recording a watched exit now queues the same settle an open runs, so the turn converges without waiting for the chat to be reopened. The reopen and read-after-restart cases now refuse that retry too, so they still pin the open's own settle. * test(native-chat): tests that pin a send settling a failed exit refuse the in-place retry too The exit's release now queues the same settle, so two tests named for the send's settle refuse that retry as well; the comments that said nothing retries it now say what does. * fix(native-chat): name the explanation row by the death it explains, so a retried settle adds no second row * fix(native-chat): end a crashed turn at its owner's provider output, never at a later send A send accepted into a crashed chat before the proof of death wrote a submission row live, and the journal-wide last-live-row bound counted it, so the revised turn ended at the send and counted Orca's downtime. The bound is now the last row the owner's provider child wrote, per writer fence: submissions, dispatch rows and crash reconciliation are Orca's or the user's, and a newer owner's work says nothing about the dead one. * fix(native-chat): end a crashed turn at its last proof of life, never at a timeline row The end of a probe-proven death was the later of the last renewal and the last live timeline row. A send accepted into a crashed chat before the proof writes a row live, so the revised turn ended at the send and counted Orca's downtime. Rows cannot tell the agent's output from Orca's or the user's, so the end is now the last renewal alone, never after the probe, and never before the turn began. The journal's live-activity bound and the reopen's recovered marker, which existed only for it, are gone. * test(native-chat): drop a stale reference to the removed live-activity bound |
||
|
|
b283a09688 |
fix(native-chat): stop flashing "still starting" on every chat launch (#23666)
* fix(native-chat): stop flashing "still starting" on every chat launch Every structured chat passes through a short startup phase, and the pane showed "<agent> is still starting…" for all of it, so a normal launch flashed the notice for a fraction of a second. The notice now goes through a keyed delayed status: it appears only once startup outlasts a grace period, stays up for a minimum time once shown, and resets per session. * fix(native-chat): reset startup notice for each provider child |
||
|
|
94b5a7d256 |
fix(native-chat): only Codex's turn completion ends a Codex turn (#23682)
* fix(native-chat): the sink queue keeps a settlement's first batch, as the journal does The journal applies a lifecycle batch's settlement id once and skips any later batch with the same id. The deferred sink queue coalesced the same key the other way: a second batch replaced the first while it was still queued. So which record survived depended on whether the first had drained yet. A lifecycle batch now keeps the queued operation with its key, and a later one is accepted and dropped, which is what the journal does once the first is written. * fix(native-chat): only turn/completed ends a Codex turn Codex follows every turn-ending `error` (willRetry=false) with a failed `turn/completed` for the same turn, 0-32 ms later. That was captured from the real app-server on 0.141.0 and 0.158.0 across eight failure scenarios, and it is how Codex builds a failed turn: it records the error as the turn's last error, records any pending input, and then derives `failed` from that error when it completes the turn. The translator ended the turn twice: once on the error, and again on the completion, with a guard to make the first end final. Ending on the error threw away what only the completion carries: Codex's duration, and the completion's receipt time. It also forgot the turn before Codex recorded the turn's pending input. Now the error is only the row the user reads, inside the still-open turn, and `turn/completed` is the turn's only live end. A process exit between the two is the existing exit sweep's observed end, recorded as interrupted. A failed completion is stored as completed with outcome failure, live and on restore alike. Only `interrupted` maps to the interrupted state. The first-end-final guard is gone. Codex sends one completion per turn, the only redelivery Orca has is the retry of a refused frame (which changes nothing), and the settlement id already keeps the first record in the queue and the journal. * refactor(codex): delete the unreachable oversized-notification settlement The translator settled a streamed item when the transport rejected its notification as oversized. Nothing can produce that frame. The Codex stdio reader frames with `maxLineBytes: Number.POSITIVE_INFINITY` (codex-app-server-record-reader.ts), which it has done since the app-server records were uncapped. With an infinite limit the framer never reports `line-too-long`: no line, pending suffix or paused queue can exceed it. So the dispatcher never emits `frame:oversized-notification`, and the arm that settles it never runs. The arm, its helper module and its test go. In place of the test, the connection test now proves the reason: a notification past the old 16 MiB wire limit arrives whole, and no oversized frame is reported. * test(codex): replace the captured ids in the turn-endings fixture with synthetic ones The replay reads ids only to group frames, so the real thread, turn and response ids from the capture account carry nothing the test needs. The fixture moves beside the Codex tests that read it. * test(codex): use a neutral made-up status as the unknown-status example 'cancelled' read as a stop being recorded as a completion. * test(codex): a restored turn with a status Orca cannot place ends with no verdict Codex's history carries the same status field as the live completion, so the restore path is pinned to the same mapping: completed, and no outcome. |
||
|
|
a5ce8251e3 |
Agent launches carry the surface that started them (#23697)
* feat(agent-launch): every launch carries the surface that started it The host now attributes every agent it builds to the surface that asked for it, resolving a missing or unrecognized surface to 'unknown' in one place instead of silently skipping it. The CLI names itself on worktree.create and orchestration workers name themselves host-side. * fix(agent-launch): attribute the agent a startup-draft create launches The host builds a third kind of agent launch: a worktree.create with a startupDraft and no startupAgent, where the host picks the agent itself. It carried no launch record at all and ignored the caller's launchSource. Route it through the same resolver as the other two builders, and derive the startupAgent terminal record only from the resolver so no prebuilt record can stand in for it. * fix(agent-launch): attribute the agent a host-built agent session launches terminal.createAgentSession builds a fresh agent's launch on the host, like the other startup builders, but spawned it with no launch record, so those launches were never counted. Record them through the same resolver; the request names no surface, so they count as unknown. * test(agent-launch): require an attribution decision for every host-built agent startup |
||
|
|
85ad292930 | fix(status-bar): re-measure collapsing levels when only the collapsed width moves (#23773) | ||
|
|
ccdb324b63 |
Add CodeBuddy as a built-in coding agent (#23740)
* feat(agents): integrate CodeBuddy launch, status and session history * docs: record CodeBuddy lifecycle verification * fix(codebuddy): backfill scoped history and negotiate remote resume * test(cli): include CodeBuddy in known search agents |
||
|
|
da48d98040 |
fix: bound combined diff editors and scope chat style invalidation (#23725)
* fix(editor): bound offscreen combined diff rendering by height * perf: scope native chat relational styles to their ancestor |
||
|
|
bd5dca4406 | fix(editor): preserve combined diff scroll on line focus (#23735) | ||
|
|
8bb78f0ddd | test(browser): create probe body before starting frame requests (#23730) | ||
|
|
4b622e1b13 |
fix(runtime): reopen the quiet-foreground tui-idle lane for agents with no other rest signal (#23598)
* fix(runtime): reopen the quiet-foreground tui-idle lane for agents with no other rest signal A tui-idle wait could never settle on a pane running amp, goose, crush, kimi, qwen-code, rovo, auggie and other agents whose titles Orca cannot classify: the quiet-foreground lane was closed for every launched agent, and it was the only lane those agents could reach, so worker start failed at agent_readiness after 60s. Model each agent's rest signal, derived from the tables that already encode it (synthetic ready titles, the title classifier, the DSH hook and Muse ready screen lanes). The lane stays closed where a stronger signal will arrive and reopens for agents with none. On a reopened lane, silence counts only after the TUI has painted: an agent that has painted nothing is still booting. Linear: STA-7440 * fix(runtime): count only the command's own output as an agent's paint on the tui-idle foreground lane The after-paint lane accepted any output, and the shell's prompt and echoed launch command always land before the agent starts, so a silently booting agent could still settle and lose its first prompt. The runtime now reads the shell integration's command-start marker and requires visible output after it; panes whose shell emits no marker keep the any-output rule. Also skip the foreground-process read while the pane cannot settle, and register the new title-classifier call site in the pane agent identity inventory. * fix(runtime): classify Freebuff's rest signal and skip the backward marker scan on chunks without one Main added the Freebuff agent after this branch point, so the full per-agent rest-signal table no longer matched on the merge ref. Freebuff derives `none`: its screen reports a first-party `done`, which tui-idle trusts only for DSH, so the quiet-foreground lane is its only one. The command-start scan ran a backward search over every PTY chunk; a forward check first cuts that to the cost of a plain substring test on chunks with no marker. * fix(runtime): classify Qoder's rest signal after merging main Main added Qoder with its own readiness branch returning a boolean quiet-foreground flag; map it to the lane type and classify Qoder by its ready screen so the full rest-signal table and lane-agreement check stay exhaustive. Say what `none` actually means: no stronger lane tui-idle trusts, not no hooks at all. * refactor(runtime): track command paint with the shared OSC 133 scanner The command-paint tracker had its own split-unsafe 133;C parser. Reuse the chunk-boundary-safe scanner, which now reports where in the chunk the marker ended, so a marker split across reads is still found. Correct the unmarked-launch list: bash and zsh mark typed launches after the echo. * fix(runtime): drop command-paint state on an output gap or a new process A dropped chunk can cut a command-start marker in half, and the scanner's carry then completes it on unrelated output after the gap, leaving the pane waiting for a paint that already happened. Reset it with the other cross-chunk carries. * fix(terminal): keep the command-start offset out of renderer lifecycle callbacks |
||
|
|
2aed2cf64a |
fix(terminal): reveal splits while the source pane binds (#23692)
* test(e2e): observe passive terminal restoration before activation * fix(terminal): reveal persisted splits during source binding publication * test(terminal): handle nullable persisted layout roots |
||
|
|
d0db35c18c |
fix(terminal): preserve typing while a remote pane reattaches (#23701)
* fix(terminal): retain typing while a parked remote pane reattaches * test: persist restored remote terminal screenshots * fix(remote): buffer recovery reconnect input * fix(remote): retain input across restored pane attach * fix(remote): flush attach input after subscription * fix(remote): flush reattach input after attach readiness * fix(remote): stop buffering after reattach readiness * test(remote): trace parked reattach input lifecycle * test(remote): forward paired client lifecycle diagnostics * fix(remote): preserve restored typing before connect starts * chore(i18n): refresh runtime required catalog * fix(i18n): ship compact agent runtime label * fix(i18n): merge required label into existing sidebar catalog |
||
|
|
d68eee3757 |
fix(runtime): retire an exited terminal before its stream end (#23492)
* fix(runtime): retire an exited terminal before its stream end An exit's durable retirement became asynchronous, so onPtyExit released the terminal stream before the retirement landed. A paired client answers a stream end by re-activating its pane; that activation still found the exited leaf, materialized it under the same session id, and registerPty dropped the pending retirement. The exited split pane came back as a fresh shell. The exit now stages the retirement into the in-memory session and publishes it synchronously, then notifies exit listeners, and only then makes it durable. A failed durable write is logged and left in memory for the next profile write instead of being rolled back, since the process is gone either way. This removes the pending-retirement latch and its post-await incarnation fence: there is no longer a window for them to guard. * test(runtime): a failed exit retirement still reaches disk Pins the no-rollback contract through a real Store and SQLite authority: when the retirement's own durable write fails, the in-memory retirement is carried by the next unrelated profile write, and by the app-quit flush when no other write happens. The delayed authority fixture can now fail its next write, and the acknowledged-retirement fixture reads the database a relaunch would load and models the quit flush. * test(runtime): a stream end observes the exit retirement already published The re-activation check alone passes with the listener ordering reverted, because activation awaits before its lookup. Record the session binding and publication count at the moment the exit listener fires so the ordering itself is pinned. * fix(runtime): an exit cleanup fault still ends the terminal stream * perf(runtime): exits retired together share one durable write * test(runtime): a refused staging write still retires the pane and ends the stream * refactor(runtime): describe exit retirement as staged, not durably accepted The retirement result is staged in memory before any write, and the removable-surface comment and the replacement-admission test name still described the old publish-after-durable rule. |