* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)
The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.
- Main stops counting a closed tab's terminal once it was asked to stop
and no pane shows it; a failed or refused stop takes the request back,
and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
feed stands in while a fresh read is out; it highlights its own
last-opened workspace and a fold never scrolls; projects file under
their project group with the desktop's indent; the collapse spinner
shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.
* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises
- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
* fix(relay-ops): keep one stalled director instance from tripping the reserve watch's 503 rule
The reserve watch's director503 rule counted fleet-wide non-drain 503s. On 10-21 07:55Z one
director instance's database calls stalled for 3 minutes; its 503s alone carried the fleet over
the line and all four watched cells flipped back to db.
Over the fleet line the rule now also needs at least 2 director instances each above a fifth of
it for the same 3 minutes, counted from Cloud Run's request log by labels.instanceId less each
instance's scheduled 503s. Above max(10x background, 200) for 3 minutes it trips with no split.
* fix(relay-ops): size the instance share to the directors seen and count platform 503s
* fix(relay-ops): size the instance share per judged minute and refuse a missing count
* fix(session-tabs): a windowless layout-owner server lists and pushes main's tab groups and order
With the layout-owner switch on and no window (orca serve, orcad), session.tabs rows came from
the snapshot hydrated from the saved session; main's layout was merged in only when a window
published. A routed reorder or Move to Split reached main's layout but not session.tabs.list or
the phone. Main now merges its layout into the rows it holds on each layout change and before
each list, with the same merge a window publication gets.
* refactor(session-tabs): keep the windowless merge with main's publisher, under the file's line cap
* WIP
* fix(session-tabs): every stored tab row follows main's layout, with or without a window
With the layout-owner switch on, the one writer of the rows phones read now merges main's
layout into every write for a workspace main owns, and main re-merges the held rows after
each layout commit on any host. A CLI rename, create or close reaches phones without a
window publish, and a row main's layout dropped cannot come back from an older snapshot.
A write that main's layout wholly overrides stores nothing, so list calls on settled rows
bump no version and push nothing. A pane's own title now counts as a manual name.
This commit also untracks a local vitest config and a scratch test that the previous
work-in-progress commit picked up by mistake.
* test(session-tabs): a rebuild from the saved session and a CLI rename keep main's groups and names on phones
* fix(session-tabs): the row writer only reorders and regroups; layout commits add main's new rows
With the layout-owner switch on, the one writer of the rows phones read now takes only main's
tab order and groups. It never adds or drops a row and never replaces a terminal id, so a
freshly started terminal keeps its id and rows the exit filters removed stay removed.
After each layout commit, on any host, the held rows take main's new rows, names and order,
keep any row main's layout lacks (closes are the close path's job), and pass any added
terminal through the same exit filters the window path uses. A main binding that runs nowhere
no longer replaces a running terminal's id. Both read main's last projection, so neither
adds a session read per write or per workspace.
A CLI close still leaves its row until the dropped-leaf hook lands; its test is marked as an
expected failure until then.
* test(session-tabs): the row writer's order and a commit merge's keep rules, unit by unit
* fix(session-tabs): a kept row keeps its group, and an unchanged commit pushes nothing
A layout commit's merge placed a row it kept (a tab main closed, until the dropped-leaf hook
removes it) by its row id, but groups list a terminal by its tab, so the row fell into the
active group. Both the writer's reorder and the merge now place rows through one shared
group-placement step keyed by the tab.
A commit now pushes only when the writer stored something. The writer skips a write only
when it keeps the stored occupant, publication and retirement proofs. A terminal row a commit
rebinds also passes the exit filters, a main binding the host saw exit no longer replaces a
running terminal, and a binding main made before its terminal connected reaches the rows when
it connects.
* test(session-tabs): a kept row's place and the no-push commit are separate tests
* fix(session-tabs): a rebuild with a new publication stamp is still skipped; simpler commit merge
The writer's skip compared publication stamps, but a rebuild from the saved session mints a
new stamp from the clock each time, so every rebuild minted a version and pushed. It now
stores only for a different workspace instance or new retirement proofs; an absent one keeps
what is stored.
The commit merge again runs the exit filters only for added terminal rows (they admit any row
the held rows list, so a rebind changed nothing), and a terminal connecting now schedules the
merge itself rather than a separate check, since an unchanged merge stores and pushes nothing.
* refactor(session-tabs): a commit merge pushes after every store again; name the window-path exit test
No reachable case was found where a commit merge's rows differ from the held rows while the
writer keeps the stored ones, so the extra "only when stored" check had nothing to guard and
goes. The exit-rule test now says it covers the window's publication path too.
* fix(session-tabs): a window reload's new publication stamp is stored, not skipped
The writer skipped a write that main's order wholly undid without comparing publication stamps,
so a window reload that republished the same rows under a new stamp kept the old one. The
reconciler then saw its accepted stamp missing and asked the window to republish on every sync,
and a host-built stamp could stay on window rows. The skip now treats stamps as unchanged only
when both are host-built, so repeated rebuilds from the saved session still mint nothing.
The windowed test of a row main's layout lacks also replays the window's older snapshot.
* Transfer keyboard focus when a sibling chat pane opens
* fix(native-chat): hand focus to newly opened chat composer
* fix(native-chat): keep tab bar controller within line limit
* fix(chat): use terminal pane opacity for split chat focus
* test(chat): verify split opacity follows the focused group
* fix(chat): an unfocused split chat dims exactly as much as a split terminal
A split chat was dimmed with the terminal's inner-pane opacity (0.9 by
default) while terminals and editors in an unfocused split group get the
group's 0.95, so chats looked dimmer than terminals beside them. The chat
layer now takes the same shared group dim and nothing more.
* fix(chat): terminals and chats in an unfocused split dim like editors
Editors and tab bars in an unfocused split group were dimmed to 0.95, but
terminals and chats are drawn in a layer outside the group, so they stayed
at full opacity. One shared opacity now dims the group panel and the
retained terminal and chat hosts alike, so all three look the same.
* fix(browser): a paired desktop shows a server's browser tab without opening a second page (switch-on builds only)
With the layout-owner switch on, a server browser tab drawn from the server's layout
reaches the desktop without a saved page handle. Showing it made the desktop create a
new page on the server, so the server went from one page to two.
Before creating, the desktop now asks the server for its browser pages and adopts the
one already named by the tab's page or browser tab id. It creates a page only when the
server has none, when the lookup fails, or when the server gives no list. A pane that
moved on during the lookup adopts nothing. Switch-off builds keep creating directly.
* test(browser): explain the fake runtime call's cast in the page session test
* fix(browser): adopt a server's browser page by the id its layout already names, not by listing pages (switch-on only)
This replaces the tab-list lookup. A tab list misses saved pages a restarted server has
not started, wakes the active saved page as a side effect, and can match a page another
client hosts.
The server's layout already records the page id on the browser tab. Desktop hydration
ignored it whenever the page record had not landed yet, and made up a new id on every
frame build. With the switch on, hydration now uses the tab's recorded page id, so the
desktop row is named by the server's page from the first frame and stays named by it.
A frame-drawn row still has no saved page handle: hydration has none for server-hosted
pages, and the server environment is known only to the pane. So, with the switch on, the
pane probes its row id with browser.tabShow. tabShow also starts a saved page the server
has not opened since a restart. On success the pane stores the handle and streams that
page. On not-found, or any other error, it creates a page as before and closes nothing.
A pane that moved on during the probe adopts nothing.
Why not reorder the server to save the page record before opening the tab: it would only
fix the id, not the missing handle, so the probe is needed either way. The page record
write also depends on the tab main writes asynchronously, so the order cannot be made
certain from the server alone.
* test(browser): explain the lifecycle harness's fake runtime call cast
* fix(browser): create a server browser page only when the server lacks it, and prefer the tab's active page id
The switch-on probe created a page after any tabShow failure, so a timeout or a brief
connection error could duplicate a page the server still runs. Now only a page-missing
answer creates one. Any other error is raised, as the stored-handle path already does, and
the pane shows its stopped state with Reconnect.
Hydration now names a tab's page by its recorded active page before its first page id,
which matches the page the server starts for a saved tab.
* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character
129 non-ASCII characters in regex literals (and three in a String.raw plugin
template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte
string. Write them as \uXXXX escapes and fail the build if any return.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(orcad): keep license comments ASCII and name the module the guard trips on
esbuild gathers legal comments at the end of the bundle; escape that comment so
a dependency's license text cannot fail the ASCII guard, while licenses still
ship with the code. The guard now reports the offset and, from an unminified
rebuild, the module the character came from. A dark-switch build is covered.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
A WSL distro is always a POSIX host and its deploy already detected Linux, so
the bridge skips the second uname round trip (100-150 ms per connect). SSH hosts
still probe exactly as before.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(renderer): read workspace records through typed selectors
* refactor(renderer): keep only the selectors with readers; type the host owner lookup
* feat(accounts): add Claude and Codex accounts to a remote server from a paired client
The server runs the login (Claude's pasted-code sign-in, Codex's device
code) and keeps the credentials; the client only opens the page and
relays the code. New capability-negotiated accounts.hostSignIn* methods;
older servers degrade to an update message.
Fixes#20009
* fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin
Review fixes: a cancel the login refused still reports the added account;
the client pins each flow to its pairing revision; Claude's stdin EPIPE no
longer crashes the host; both sides accept only the provider's sign-in
host; pasted codes reject whitespace; a host-side supersede says so; Codex
cancels its own login handle; opt-in real-CLI test.
* fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only
A Cancel clicked before the host answers the start is applied once it does;
the client keeps polling through a short outage until the host would have
ended the flow; Claude's link is read from stdout only; a timeout after the
code was sent keeps its own message.
* test(accounts): name the stdin sender and allow its stream write in the persistence ratchet
* fix(accounts): register each Add Account attempt before its first await
One attempt per click owns the pairing pin, the cancel flag and the host's
flow, so a Cancel during the capability check or the start is never lost;
polls retry only lost answers; the stdout forwarder is removed on cleanup.
* refactor(accounts): drive the client sign-in as one explicit state machine per server and provider
checking -> starting -> following -> settled, with Cancel as a flag the run
loop delivers once the host names its flow and re-asks after any answer
until the host takes it. A second Add joins the running attempt; a Cancel
the server could not be told about is surfaced; retry policy tested.
* fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends
* fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow
The host picker can move while a sign-in runs: Cancel now targets the
running server sign-in (or the desktop's own login when none runs), and
the pasted-code form is keyed by server and flow.
* fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning
* fix(accounts): the server sign-in prompt owns its Cancel
Cancel for a server sign-in now lives in the prompt bound to that server,
which also reappears with its Cancel when Settings reopens mid-sign-in; the
header Cancel is only for this desktop's own login.
* fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status
* fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs
* fix(accounts): the header Cancel follows the running addition, wherever Settings now points
Each Add Account records where it is signing in. Cancel stops that
sign-in, on a server or this desktop, and is hidden only where that
server's own prompt already offers Cancel.
* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane
After an SSH reconnect the pane is repainted from the host's screen
snapshot. When the host cannot give one (too large for the replay limit,
no buffer, unanswered, overflowed), the pane only printed a loss banner and
an idle full-screen program that writes diffs kept its pre-disconnect frame
until the user resized.
The two places a remote pane declares it has no host image now ask the
program to repaint through one choke point: a one-row viewport claim and
back, which every TUI honours (Node's tty ignores a same-size SIGWINCH).
It respects a phone or another desktop holding the size, skips hidden
panes, and puts the row back even if the pane unmounts mid-nudge.
Fixes#26887
* test(e2e): idle full-screen program frame after a managed SSH outage
* test(e2e): wait for the pane before typing in the idle-frame spec
* fix(remote-terminal): pick the redraw height on the host's clamped grid
* fix(remote-terminal): wait for the host's grid before putting the redraw row back
* test: give transport mocks a connection status for the redraw gate
* test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet
* fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge
* test: keep new transport-mock lines free of type assertions
* ci(e2e): route the managed-SSH idle-frame spec to its sources
* fix(layout): a running agent's resume record is not a sleep in switch-on builds
The shared layout load put every pane resume record into leaf.sleeping, so main's
terminal index read a live or quit capture as a slept pane and terminal.list
reported running agents as disconnected and unverifiable. Records of slept panes
(isPaneSleepRecord) now load to leaf.sleeping and running agents' records to
leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the
disk format is unchanged. The disk-field census names both holders, and a model
rule refuses a pane holding both.
* fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record
* fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake
Adopt the nine Japanese wording corrections from community PR #29248. Clarify smartphone control, manual restoration, and mobile data connections.
Closes#29247.
Co-authored-by: sojiro-o <53679703+sojiro-o@users.noreply.github.com>
Fixes#18283
Supersedes community PR #18347. Applies the existing desktop WebGL patch to the pinned mobile engine after independent adversarial and upstream review.
Co-authored-by: fettpl <38704082+fettpl@users.noreply.github.com>
On a headless host the mobile status builder treated any title without an
agent marker as the shell reclaiming the pane. Codex's idle title is its
project name, and since OSC 7501 program status gives the pane a PTY state,
a Codex turn with no tool call ended with the hook's transcript address
withheld, so the phone showed "Can't find this conversation yet".
Only a shell or management title now retires the hook identity, matching the
renderer projection. Refs #31136.
* Measure Windows ARM qualification worker concurrency
* Reuse Windows server prebuilds across app version bumps
* Match native cache measurement to adhoc compiler settings
* Keep measured cache fix and remove temporary comparison modes
* fix(native-chat): typing fast after focus moves keeps keys in order
Typing a burst right after focus left the composer (Move Tab to Split, a
paired chat, a click on the transcript) could scramble the first letters:
"Reply exactly" came out as "Rply exactly...e".
The first key was routed into the composer programmatically: focus moved
at once, but the caret was set a frame later from a position computed
before the key. The next keys landed natively, then that late caret write
pulled the caret back. Quote "Add to chat" and picker completion had the
same deferred caret write.
Each programmatic write now puts the text and caret into the editor in one
synchronous step, read from the editor's live value, then focuses it. No
write happens while an IME composition owns the field.
* fix(native-chat): retain saved chips after synchronous composer edits
Retire old terminal stream callbacks and delayed fits across reconnects, unsubscribe, terminal replacement, and synchronous termination. Preserve fresh output and the latest accepted fit.
Fixes#30505
Adapted from community PR #30506.
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Let the finger that creates a long-press selection extend it through the existing selection-drag dispatcher. Verified with native iPhone touches and focused lifecycle tests.
Fixes#30554
Adapted from community PR #30555.
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
* fix(ssh): the host's 'no desktop is sharing its browser' error names the browser routing setting too
The desktop withholds the browser bridge from a managed SSH host whose pages browse from the
desktop's own network, but the host's CLI was told only to turn on the per-host CLI control
switch, even when it already was on (#31421).
* fix(ssh): name the per-host routing opt-out too, and guard the labels against UI copy drift
The runtime WebSocket listener widens to 0.0.0.0, which never accepts
IPv6, yet the Connection address picker and the default pairing address
offered global IPv6 interfaces, minting links nothing answers. Filter the
interface list by the family of the host the listener binds when serving
the network.
Reported-by: @fongyuan-2011
* Prepare template assembly during qualification and cache Intel downloads
* Limit early template qualification polling to fifteen seconds
* Verify Windows retains its download cache policy
* Drop Intel store restoration after measuring its overhead
* fix(plugins): a development plugin reloads an edit saved while its watcher was starting
Orca scans a development plugin folder, then starts watching it. The watcher goes live
about a second later and reports only changes made after that, so a save in between was
never picked up and the plugin kept running the old files. Once a development root's
subscription goes live, schedule one catch-up refresh through the existing debounce.
* test(plugins): count a replaced dev root's catch-up refresh regardless of timing
Preserve UIKit recognition placeholders during equal-text Fabric synchronization and reset recognition state when the native input is recycled.
Adapted from community PR #26644 by Shahar Mor. Verified with matched source-built native Debug lifecycle probes for both input classes; physical spoken dictation remains in the final manual batch.
Co-authored-by: Shahar Mor <shaharmor1@gmail.com>
* fix(native-chat): a Claude chat worker keeps running after Claude acknowledges its task
Claude's adapter dropped an echoed send from the sends it holds before
reporting the send's answer to the host, and emitting the echo's turn
can publish at once. That publish read the still-pending send as held
by nobody, so it read as "couldn't confirm": orchestration worker-start
then closed the new Claude worker, and every other answer wait and
client saw a one-frame doubt.
A frame's settled sends now stay held until their answers are reported,
when the host's handover window takes them over; a Stop's rejected
sends are released and reported one at a time, as Pi and ACP already do.
Tracked in STA-11160.
* test(native-chat): assert the settled send's dispatch state through the send result
* Reapply "fix(native-chat): Claude, Pi and ACP chats are Working only while the agent holds the send (#30134)" (#30572)
This reverts commit 3ef5a46061.
* fix(native-chat): a Claude send stays held until its settlement reaches the host, on the reapplied held-sends read
* fix(native-chat): a Claude send stays Working, not in doubt, while a Stop is still ending its CLI
* test(pi): drop the removed idle-release dependency from the adapter fixture
* feat(layout): main places a swept workspace's live terminals; the window sweep only reads (switch-on builds only)
The window's activation sweep (first visit, an empty or sleeping activation, a phone wake) now
asks main, through its existing terminal.list call, to place that workspace's unshown live
terminals before listing them. With the switch on the sweep writes nothing to the window store and
counts a pane main names as shown, so it no longer seeds a fresh shell beside a terminal main just
placed. Main's "still binding" check also covers a registration in flight.
* fix(layout): a sweep's placement pass holds back only its own workspace's first surface (switch-on builds only)
* test(layout): switch-on activation-gate and census-call tests read main's listing, not window binds
* fix(ssh): draw every host list through one bounded, searchable host picker
A generated ~/.ssh/config can import 13k+ SSH hosts, and several surfaces drew a
row per host: Settings > General workspace directory 'Apply to', Notifications >
Machines, the sidebar and Agents Hosts submenu, the composer run-target picker,
the Add Project host selector and remote step, the skill-install machine pickers,
Settings > Servers SSH host picker, repository 'Add to another host' and the
automation host field and filter.
They now share components/host-picker: boundHostList (pinned hosts plus a ranked,
capped match list), HostCombobox for single choice (a Command in a Popover that
mounts nothing until opened, replacing Selects that mount every item), and
useBoundedHostRows with HostListSearchField and HostListOverflowNote for
checklists. A host-list-render ratchet stops new JSX from mapping straight over a
host collection.
* fix(ssh): host lists keep room for search results, menu search is keyboard-reachable
- boundHostList gives pinned hosts and other matches separate limits, and narrows
pinned hosts by the query, so a large selection never hides what the user types.
- In a dropdown, the host search is a menu item: arrow keys reach it, focus moves
into the input, and arrows leave it again.
- The Add Project host picker hides its setup actions while searching, so Enter
picks the matching host.
* fix(ssh): re-picking the current host in a host picker is not a change
The Selects these pickers replaced suppressed same-value changes; the skill
install form clears its workspace and target on a machine change.
* chore(ci): let the shared ratchet runner pick up the host-list render ratchet
main now discovers every check-*-ratchet.mjs through run-ratchets.mjs, so the
separate workflow step would run it twice.
* fix(ssh): host lists rank pinned matches and keep input order; label and scroll fixes
- boundHostList ranks pinned and other hosts by the same match order before
cutting each to its limit, then keeps input order among equal ranks, so a
muted or checked exact match is always reachable and clicking a row never
moves it.
- The Servers SSH host picker is named by its form label again.
- The sidebar Hosts submenu scrolls its host rows instead of running off screen.
- The composer's overflow note sits outside the listbox.
* fix(worktree): refuse a parent loop even when the worktree cache is cold
The parent cycle walk read each workspace's current instance from the
resolved-worktree cache, which the reparent itself invalidates, so it only
knew the two endpoints and accepted A->B->C->A. Read saved worktree
metadata instead.
* fix(worktree): fall back to locator metadata and tolerate loops saved earlier
* fix(worktree): skip another host's locator metadata in the parent loop walk
* fix(worktree): read loop-check identities from a read-only, ownership-scoped view
The per-host metadata accessor migrates a hostless legacy row into the
queried host, so the loop check could adopt another host's worktree and
refuse a valid parent. Read one read-only host snapshot instead, counting
a row only when its project is registered on that host (the resolved
list's ownership rule).
* fix: import host-qualified worktree meta helpers once
* test: match main's host-targeted lineage write contract
* fix(shortcuts): app shortcuts run before the terminal, whatever order they mounted in
App shortcut owners and terminal panes both listened on window capture,
so React's effect order decided who saw a chord first. After a worktree
switch the new pane registered before the tab shortcut listener and
turned a user-bound Cmd+Right into Ctrl+E; clicking a tab flipped the
order back. A keyboard router installed before React renders now runs
every app owner, in registration order, ahead of any pane listener; a
key no owner claims still reaches the terminal's own handling. Held tab
chords are claimed on every repeat but switch once.
Tracked in STA-11205 (same bug as STA-4602).
* test(shortcuts): type the router mock with key event names
* fix(shortcuts): install the keyboard router in the web client before it renders
* test(shortcuts): floating panel specs read the routed key listeners from the window stub
* fix(notifications): tie a completion banner to the hook turn it announces, not a 10 s window
A title-only working->idle edge (Claude's /btw side question fires no hook) raised a
generic "Task complete" banner because the pane's hook row was older than 10 s. On a
hook-reporting pane the hook row now decides: a row not yet announced supplies the
banner content however old it is; an already-announced row means no turn ended, so
no banner. Hookless agents keep the generic title fallback.
* fix(notifications): a pane with hooks gets its completion banner from its hook turn only
The title lanes (mounted pane and parked watcher) announced a finished task on every
working->idle title edge, even on panes whose agent reports turns through hooks. A title
spin that ends no hook turn (Claude's /btw side question, Codex's /compact) raised an extra
banner that could only be filled with the workspace name and tab title.
On a pane the hook lane is tracking, with a settled Done row from the same agent, the title
lane now defers before it claims the turn, so the hook lane's banner (with the last message)
is the only one. A BEL in the same burst waits on the hook lane's pending Done. Hookless
agents, restored rows, hooks switched off and the post-restart re-arm keep the title banner.
This replaces the previous commit's announcement memory in the dispatcher.
* fix(notifications): hook-owned turn ends settle pane bookkeeping and announce re-entered Stops
- A title idle edge that defers to the hook lane now settles the pane's turn, so quitting the agent after a /btw no longer raises a generic "Task complete" banner.
- A BEL held while the hook lane's Done is pending is superseded by that Done at fire time instead of being held until the next turn start.
- A Stop re-entered after a blocking Stop hook carries its new final message in its identity, so the hook lane announces it instead of deduping it against the first Stop.
- Title lanes ask the hook lane whether it can announce the turn end (live, agent evidence, not re-arming) instead of copying its conditions, and fall back when the agent's own hooks are switched off.
- The hook lane's non-quiet Done forwards its own snapshot, so its banner content no longer depends on the stored row's age.
- The whole-flow test now drives the real pane wiring, the real hook lane and the real dispatcher.
* fix(notifications): the host marks each real turn end, so a repeated status can't raise a second banner
The renderer told a Stop re-entered after a blocking Stop hook from a
restatement by its final message. The message also changes when the
listener's cache resets (a session reset, then an idle reminder), so a
restated finished turn raised a second, empty "finished" banner.
The execution host now classifies each provider's own turn-end event by
name and stamps an optional `turnEndedAt` on the status row. Idle
reminders, session events, replays, child re-emits and result retries
restate it; a turn-end event on an already finished row is a new end only
when the agent says it re-entered (`stop_hook_active`). Local hosts and
SSH hosts (orcad) mint it in `attachStatusTiming`; the WSL relay mints it
at admission and sends it on its envelope. The field is persisted,
forwarded over IPC and the paired-server session projection, and the
renderer's done identity is `turnEndedAt ?? stateStartedAt` (after the
existing `turnCompletedAt` precedence). `stateStartedAt` and every other
clock are unchanged.
Paired servers that predate the stamp advertise no
`agent-status.turn-ended-at.v1` capability; their panes keep the
terminal-title completion fallback exactly as before.
* test(notifications): type the old-server pane setup in the completion flow test
* fix(notifications): a blocking Stop's final banner reaches the user; /compact and quitting an idle agent stay quiet
Real-agent QA on the hook-only completion design found three gaps:
- A Stop re-entered after a blocking Stop hook could be dropped twice
over: inside 1 s of the first banner by the replay guard (a new host
turn-end stamp is now a real end), and by main's 5 s per-workspace
desktop cooldown. A banner with the same notification id and changed
text now replaces the one already shown instead of being dropped;
every other cooldown rule is unchanged.
- On a mounted pane, a hook-owned idle edge (Codex /compact) no longer
claimed a completion, so the burst's BEL went out as a Bell banner. The
deferred idle edge now absorbs that BEL like a claimed completion did.
- Quitting an idle agent after a title spin raised a generic process-exit
banner. On a pane whose hook lane owns turn ends and whose last turn is
done, an exit ends no turn; a crash mid-turn still gets its banner.
* test(runtime): the projection reference keys on the host turn-end stamp too
* test(relay): restore the Copilot retry test while CI isolates a runner error
* test: key the hot-path projection reference on the turn-end stamp; relay retry restates the end
* fix(notifications): host turn identity per agent session; keep later blocking-Stop banners past main's cooldown
- The execution host (local main and the WSL relay, one shared reducer) now tracks which turn an
agent session is in: Codex by its own turn id, Command Code by its transcript prompt position,
every other agent by its prompt-opening hook. A turn end mints a new end when its turn differs
from the last ended one, so a Codex turn woken by agent mail and a text-only Command Code turn
get their banner; duplicates, replays, tool reopens and result retries restate it. The record
is saved with the status row (validated on restore) and never published.
- Main's desktop and mobile notification cooldowns are back to main's code exactly.
- A later end in the same done segment (a Stop re-entered after a blocking Stop hook) that lands
inside main's cooldown is held, latest only, and sent once after that cooldown through main's
unchanged gates; the renderer learns main's answer to anchor it.
- One pane check gates every changed pane behavior: the pane's bound PTY runs on a host that
stamps turn ends and its agent's continuation signal is verified (Claude, Codex). Every other
pane, and an unbound PTY, keeps main's behavior.
- A hook-owned idle edge absorbs its burst's BEL for main's full 1.5 s; exits and replacements
ask the owner check with the exiting agent; a title spin on a hook-owned pane opens no turn.
* test(notifications): run the whole-pane harness through main's shared desktop cooldown, not main's service module
* fix(notifications): decide hook ownership from how the host identified each turn end
The execution host now labels every classified turn end with one optional
disposition beside its end stamp, by the same rule for every agent:
identified (an unused prompt opening, a new native turn id, or a verified
re-entered Stop) mints a new end; a same-turn repeat is a proven duplicate
only when the agent supplies both a turn id and a verified continuation
flag; anything else is ambiguous and never mints. Replays and retries
restate the decided answer, and local and relay hosts carry an end only
within a done segment.
The renderer replaces the Claude/Codex pane gate with one evidence
decision: identified ends are hook-owned, ambiguous ends keep the title
lane's role, and rows without a disposition (older hosts, unclassified
agents) keep the previous behavior exactly. A title working edge on a
hook-owned pane keeps its turn bookkeeping and only the hook receipt; an
owned idle edge settles after the existing max wait, or announces through
the title path if the end turned out ambiguous. Idle and working title
edges resolve the same owner (the title's agent, else the foreground one).
The held later banner for identified continuations is unchanged.
* test(notifications): give the whole-pane harness's host event its local connection id
* fix(notifications): a settled cancellation ends the open turn, so its late Stop folds as on main
A cancel Orca inferred from Esc or Ctrl+C writes a done row with no turn-end event. The host reducer now completes the current turn key on that settled cancellation, so the same turn's late Stop is a repeat (ambiguous, no new stamp) instead of a second identified end that requested a second banner.
* fix(notifications): a settled cancellation ends the open turn on every lane, so a relay's late cancel end restates none
The desktop restates a WSL relay's end as decided. The relay never learns a
cancel the desktop inferred for a non-Claude agent, so Grok's own late
stop_cancelled reached the desktop as an identified end and gave a second
banner. One predicate now decides both lanes: after a settled cancellation
that carried no end, a restated end keeps the shown one, and a child event
carrying the cancellation also ends the open turn. The relay's own cancel
row is the main agent's, never a child's.
* fix(notifications): a relayed done segment shown without its own end keeps showing none
The restatement fold tested the previous row's cancel verdict, so the row it left behind (a late
plain stop) no longer matched and the relay's next restatement brought its end back. Fold on the
row's turn-end fact instead, which the folded row satisfies again.
* fix(mobile): remove the chat's Tools toggle and its open-everything mode
* test(mobile): keep the scroll-ownership tests' runs drawn without the Tools default
* test(mobile): the scroll-ownership harness opens settled turns as the old Tools default did
* fix(sidebar): a slept chat workspace reads asleep and hides
* fix(sidebar): a wake during Sleep's teardown keeps the workspace awake
* test(sidebar): give the activation-race mock store the chat tab map Sleep reads
* feat(native-chat): the host marks the chats an explicit Sleep stopped
The fact "the user's Sleep put this chat to rest" now lives on the host
that runs the chat, on its session record, instead of in the window.
- New runtime capability agent-session.workspace-sleep-mark.v1 and an
optional markAsleep on agentSession.sleepWorkspace (strict params, so
only hosts advertising the capability are sent it).
- The Sleep close decides each chat's receipt inside the same per-session
step that stopped it; with markAsleep it writes sleptAt in that step's
record write, so a chat is marked exactly when it is in the receipt.
- The next send the host hands over clears sleptAt, whichever client sent
it; the idle sweep and plain stops never mark.
- sleptAt rides the status summary (optional field), its equality, and
the retained summary of a closed chat, so it survives restart through
the persisted tab index.
* refactor(sidebar): read a slept chat from its host's mark, not a window record
The window no longer keeps its own saved record of which chats Sleep
stopped (explicitlySleptChatsByWorkspace). That record had to mirror every
host event that should end it (sends, replaced conversations, renames,
profile and WSL moves, removal, wakes during teardown), and each missed
mirror was a bug. The host now owns the fact (previous commit).
- Delete the record, its schema, store slice, workspace-session field
registrations, moves, renames, purges, migrations and hydration, and the
renderer clearing hooks. Those files are back to main.
- Opening a workspace no longer wakes its slept chats (product ruling):
drop the chat Sleep-attempt tokens and their set-active-worktree hook. A
slept chat wakes only when a send reaches its host, from any client.
- The status bridge mirrors each chat tab's published sleptAt into a
store map keyed by the chat's pane key, dropped when its projection
unmounts and never saved; a chat whose host runs it again is not slept.
- The asleep derivation, hide filter, palette, board and context menu read
that map. Explicit Sleep sends markAsleep only to the local host and to
hosts last probed with the new capability; a project move never does.
* fix(native-chat): preserve host-owned Sleep across fallback and legacy tabs
* fix(native-chat): clear Sleep when a new send is admitted
* chore: restore incoming documentation after merge hook formatting
* fix(native-chat): let admitted sends supersede pending Sleep receipts
* fix(sidebar): wait for host Sleep state before showing restored chats
* fix(sidebar): retain host Sleep answers when chat projections remount
* fix(native-chat): clear Sleep when queued drafts become sends
* test(sidebar): cover unknown and known chat Sleep state on the board
* test(vault): expect both host status capability checks
* test(native-chat): consolidate Sleep behavior coverage
* fix(native-chat): commit Sleep wake with send admission
* test: pin visibility while chat host is unidentified
* test: consolidate workspace sleep regression flows
* test: keep status feed regression within renderer boundary
* Parallelize adhoc Mac architecture packaging
* Include parallel Mac jobs in release timing reports
* Cover parallel Mac publication and failure gates
* Explicitly scope archive targets to each Mac architecture
* Editor: git Discard pauses that file's autosave in every workspace; read-only markdown/CSV never pushes over edits (phase 1b-3)
- A Source Control discard holds autosave (and waits for in-flight saves) for every tab of that
file in every workspace, keyed by its model save key, until the discard finishes or fails; the
quiesce now returns the release, called in a finally. Watcher notices don't release it. A 120 s
backstop only shows the changed-on-disk banner on dirty tabs and never reopens autosave. This
closes the race where the compare fallback on older hosts reads disk just before git restores it.
- A read-only tab renders raw Monaco source whatever its type, so it counts as a code view: a clean
read-only .md/.csv tab can no longer push disk text over a writable tab's unsaved edits.
Only removes or delays writes. One autosave timer per model moves to 1b-4.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold review: release on a failed quiesce, cover unstaged diffs, re-show an overdue banner (phase 1b-3)
- A quiesce that throws (e.g. an unresolved CSV cell) releases its hold and rejects, so the discard
reports the error instead of hanging with autosave off; a bulk discard frees the holds the other
paths took.
- Holds key on the physical file (host + path), so an unstaged diff tab of the file in another
workspace is paused and drained too; save-queue keys are unchanged.
- Past the backstop, a dirty held tab whose banner was dismissed gets it back, so the pause is
never silent. Taking a hold no longer re-syncs; the save-time check already refuses.
Co-Authored-By: Claude <noreply@anthropic.com>
* Editor discard hold: key the physical file with the 1b-1 path rules, not the client's URI rules (phase 1b-3)
The hold key went through the Monaco model URI, which normalizes paths by the client platform, so
on a Windows client `/repo/a\b.ts` and `/repo/a/b.ts` on a POSIX host shared a key. It now uses
the owner key plus `editorBufferOwnerPathKey` (new, in editor-buffer-identity), which applies the
1b-1 rules: POSIX backslashes kept, separators folded only for proven-Windows paths, UNC server
case, and WSL aliases on the local Windows client. Same on the tab and the discard-path side.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ci: one list for Windows boundary tests and one step for ratchets
The Windows boundary step's 74 test paths were hand-listed in pr.yml and
again in WINDOWS_PACKAGE_TESTS, and the copies had drifted: six files ran
in the step but could not trigger the job. The step now prints
WINDOWS_BOUNDARY_TESTS, which WINDOWS_PACKAGE_TESTS spreads.
The ten per-ratchet preflight steps and the eight lint entries become one
run-ratchets.mjs that runs every check-*-ratchet(s).mjs it finds.
* fix(ci): preserve existing local lint ratchet scope
* fix(terminal): hidden tabs reconnect to sessions that survived a restart
pty:hasPty answered from the provider's attached-this-run set, so right
after a restart every surviving session read as exited and #29459's
hidden-agent hold kept live hidden tabs disconnected until shown. On an
attached-set miss, ask the host through the provider's existing liveness
probe; providers without one (the in-process provider, its own sole
owner) keep their answer.
* test: remove liveness fixture type assertion