* fix(ssh): a relay-era desktop can join the server another desktop on the same login runs
A desktop that still had relay-era projects on a host whose managed server
another Orca version ran could never connect: its migration skipped the
running-version adoption, so it deferred on a terminal count only a paired
server can give. Once the versions matched it failed instead because the
server already held the same repository path, added by the other desktop.
The migration now pairs with the running server like a fresh desktop does,
before any upload. Projects the server already holds at the same host path
are linked to the server's project instead of imported twice: the journal
is re-cut without them, under the same migration id, only after the server
proves it holds nothing of the earlier manifest. A conflict the server will
keep refusing now says what conflicts and how to get past it.
* fix(ssh): never link a project a bridged relay terminal still runs in
The bridge reattaches a relay pane only into its own tab, which a linked
project never sends to the server, so linking would leave that terminal
running out of sight. Refuse the link instead, naming the project and how
to end the terminals.
* test(ssh): run the link test on Node's SQLite runtime and read the host's activation record
* test(ssh): establish desktop B's profile before seeding its relay-era state
* test(ssh): connect desktop B's host explicitly in the shared-login spec
* fix(ssh): say what a linked project left behind, and never blame it for an unowned relay terminal
A project this desktop moved that the host's server already held is linked to the
server's copy, so its tabs and automations from this desktop stay behind. The
connect now carries those linked projects and the SSH host card says so, with the
automation count, instead of dropping them silently.
A bridged relay terminal of unknown or removed project still blocks a link, but its
guidance no longer names the linked project as where it runs. Both terminal
messages now offer removing the project from this desktop as the way out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The layout stream (layout.subscribe, still unused) now carries the title each
pane's terminal shows now, as an optional liveTitle on the pane. It is laid
over the stream's cached projection with its own trigger, so a title change
reads no Store data and projects no partition; changes in one task coalesce in
the existing microtask flush. The trigger is the per-PTY fan-out that already
skips spinner-only title frames, so an agent's spinner sends no frames. The
latency spec gains a title-flood phase.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(layout): drop a legacy terminal row the tab bar already holds when loading the layout
The shared layout loader turned a stored terminal row with no tab-bar entry into a tab even when
every terminal it names is already shown by a tab-bar tab. The window drops such a row at startup.
The loader now uses the same rule, moved from the window's row hydration into
src/shared/workspace-layout/legacy-terminal-row-claims.ts so both read one implementation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(layout): follow a dropped legacy row's selection to its tab-bar twin
When the loader drops a row the tab bar already holds, the saved active tab (global and per
workspace) can still name that row. The window's startup moves those selections to the tab-bar
tab that holds the same terminal; the loader now does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters
A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.
* test(e2e): dedupe sidebar row ids in the visibility spec
* fix(mobile): address review: owner naming, old-server relayed phones, scope trim
* refactor: move the sidebar's project section grouping into shared
The project sections the desktop sidebar draws under Group by Project are
built from the project/setup projection, the per-host catalog fold, the
endpoint host stamp, and the section labelling and ordering rules. Move the
pure parts of each into src/shared (no behaviour change) so the paired phone
can call the same code instead of guessing the grouping.
* fix(mobile): group by Project with the desktop sidebar's own sections
The phone grouped Project sections by repo display name, so it merged
checkouts the desktop keeps apart (several local stablyai/orca checkouts,
or a server's copy) and split what the desktop merges. The phone now reads
each host's repo.list, project.list and projectHostSetup.list (the desktop's
own and each server's through the relay), folds them with the renderer's
shared catalog fold and projection merge, and builds sections with the
desktop's shared grouping, labelling and ordering code. Keys, labels and
Recent order equal the sidebar's; a host that predates the project lists
gets the repo-derived projection, as the desktop does.
Header colour and icon come from the section's anchor repo and rows from
their own repo by id, as on the desktop. The repo filter lists every
catalog repo by id per host instead of merging same-named repos. Group-by
labels read Project and None. project.list and projectHostSetup.list are
routed for paired phones (read-only, workspace permission).
* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag
A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.
* test(e2e): poll the phone and the sidebar together in the visibility spec
* fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order
Review fixes. The sidebar's buildOrderedGroups now calls the shared
buildProjectSections for Group by Project (notice and pending repos are
optional inputs), so desktop and phone run one function. The phone ranks
manual header order by its folded catalog order, which is each host's
repo.list order with the desktop's own repos first, as the renderer loads
them; only the desktop's cross-host manualRepoOrder (pairing-local) is
unknown to it. The projection selector is now a memo over the shared
resolver. A transient project-list failure keeps that host's last catalog;
only forbidden or method_not_found falls back to the repo-derived
projection. Catalog rows are checked on every member grouping reads, and
the parity test answers the renderer's real catalog fetch, includes a
server's own project rows, and fixes Manual and Recent orders that differ.
* fix(mobile): use the repo-derived catalog until a host's project lists first answer
* fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it
The phone fetched repo.list, project.list and projectHostSetup.list from
the desktop and every relay-ready server each minute and folded them
itself, so rows from offline or older servers fell back to repo sections
and keys moved on cold start.
The desktop renderer now publishes the inputs its own Project headers use
(state.repos in sidebar order, the projection selector's projects and
setups, projectOrderBy) on the runtime window graph it already sends to
main, only when they change. Main keeps the last one and answers it on a
new paired-desktop read, mobileRelay.projectSections. The phone reads it
with its repo metadata and groups with the shared builder; a desktop that
predates the read (forbidden or method_not_found) leaves the phone grouping
rows by repo from the desktop's repo.list, and a transient failure keeps
the last catalog. The payload carries only grouping and label fields, no
hooks, credentials or uploaded icons. The phone's own catalog fetching and
its project.list/projectHostSetup.list routes are removed.
* fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests
* test(e2e): the desktop window serves its sidebar's Project catalog to the phone
* chore(i18n): regenerate the runtime English catalog (drift already on main)
* fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow
The extra read inside the repo-metadata refresh stalled its recorded RPC
sequence. It now runs beside it with its own throttle. The repo-metadata
goldens are re-recorded for the intended state change: the name-keyed
colour, icon and id maps became desktopRepos.
* fix(mobile): fetch the Project catalog at once when the screen moves to another host
The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers.
* fix(files): never write through a dangling symlink
A path that doesn't exist yet was authorized by walking up to the nearest
existing folder. A dangling symlink fails realpath with ENOENT, so the walk
stepped over it and approved the link's own path; the write then followed
the link and created its target outside the allowed folders. The walk now
continues from the link's target, and returns that real destination.
* refactor(native-chat): rename the chat visuals folder to the chat scratch folder
The per-chat folder agents write into is the chat's scratch space, not only
a visuals folder. Its root becomes <state>/chat-scratch/<hash> (the hash is
unchanged) and the agent variable becomes ORCA_CHAT_SCRATCH_DIR. The old
ORCA_CHAT_VISUALS_DIR is never set and is still stripped from what every
agent child inherits, Pi included. The visuals skill names the new variable
and says the folder is temporary.
* test(native-chat): keep the delivery test to the rename
The launch parity snapshot already pins that every agent drops the
pre-rename variable.
* feat(native-chat): chat scratch folders expire after 30 days unused
A chat's scratch folder was removed only when its record was gone or its
local workspace was proven deleted, so the folder of a long-idle chat
lived forever.
The sweep now expires a chat's folder once its newest journal row and
the newest entry anywhere in the folder (complete, no-follow scan) are
older than 30 days, unless the host holds a running agent, a delivery or
an open dispatch for the chat. The final recheck and a rename to
<hash>.expired run in the chat's session lane and then a per-folder lock
that Orca scratch saves (desktop IPC and host RPC) also take; the delete
runs outside both. A save that settles after the age scan marks that
expiry stale. Leftover .expired folders and the pre-rename
native-chat-visuals root are removed.
The workspace-removed rule and its catalog verdict are deleted.
* fix(files): refuse any dangling symlink in the missing-path walk
Following a dangling link by hand resolved relative targets against the
lexical parent, not the link's real folder: through a folder link the walk
could loop forever or authorize the wrong file. Refuse instead: realpath
can't say where a dangling link leads, but a write would follow it.
* test(files): drop the loop case that duplicated the dangling-link refusal
* refactor(native-chat): trim the scratch expiry and test the real live-work veto
- Remove the other-profile catalog options (running profile id, never-written
profiles) that only the deleted workspace verdict used; history cleanup's
reader is unchanged.
- The sweep keeps its original remove-and-warn block; an unused chat's
folder is parked by one function. Drop the unused `now` option and the
candidate identity check.
- The live-agent case runs against the host's real runWhileExecutionIdle.
* style(native-chat): keep the session host within its line limit after the main sync
* fix(native-chat): every scratch save and every overlapping sweep share the folder lock
Binary (base64) runtime saves now take the scratch folder lock like text saves, and each folder
keeps a set of expiry candidates so one sweep finishing no longer hides a save from another.
* fix(mobile): size markdown table columns to their content
* fix(mobile): always show the copy button under chat messages
Revealing it on tap shifted the transcript.
* fix(mobile): read markdown with a CommonMark parser
markdown-it rather than desktop's micromark: about 8x faster on the phone's JavaScript engine.
* Scope Windows native compiler setup and measure ARM inbox preparation
* Add a controlled Windows native preparation comparison
* Test native preparation through an injected process runner
* Guard reviewed Windows native batch runners
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters
A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.
* test(e2e): dedupe sidebar row ids in the visibility spec
* fix(mobile): address review: owner naming, old-server relayed phones, scope trim
* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag
A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.
* test(e2e): poll the phone and the sidebar together in the visibility spec
* feat(native-chat): copy chat selections when Copy on Select is on
The terminal's Copy on Select setting now also copies text selected in a
native chat transcript, at the point the Add to chat offer already treats
a selection as finished. It writes the same text right-click Copy does,
skips the composer and other fields, and leaves the setting key as is.
* fix(i18n): translate the Copy on Select description and search keywords
Settings and Settings search showed English to es, fr, ja, ko and zh users
after the description moved to new keys. Extend the old terminal-only
translations to cover chat, and translate the new highlight and chat search
keywords.
* fix(native-chat): send typing after a click to the chat's composer
The default Claude/Codex chat never wired the root typing redirect, so a
click in the transcript, on a toggle or link, on the chat's own tab, or on
blank app chrome left typed keys with nowhere to go. The chat-view-over-TUI
had the redirect but treated every button and link as a key owner.
Both views now share one hook that runs the reveal focus, focuses the chat
on blank clicks, routes keys typed inside the chat (but not in overlays it
portals out), and, while it is the only chat visible in the focused group,
routes keys typed with focus on the page body. Buttons and links pass
letters but keep Space and Enter. Re-activating an already visible chat tab
re-runs the composer claim, as a terminal tab refocuses xterm.
* fix(native-chat): refocus the composer when its active tab is activated again
Re-activating an already visible chat tab re-ran the ordinary reveal
claim, which stops as soon as focus is anywhere inside the pane. With the
transcript or a code-block Copy button focused, the tab shortcut left focus
there and Space kept scrolling or pressing the button. The activation now
moves focus to the composer from inside the pane too, unless a text field
such as the find input holds it; an ordinary reveal still keeps the user's
focus.
Each Tasks sheet was its own native modal, and iOS refuses to present one
over another that is still showing. They now share one modal and stack by zIndex.
Fixes#21003
* feat(wsl-mover): shell history moves with the project to the WSL host (D1b-6c-1)
* fix(wsl-mover): moved WSL history lands under the host's login shell (D1b-6c-1)
* fix(wsl-mover): history follows the shell a pane would actually run, fallback included (D1b-6c-1)
* fix(wsl-mover): history installs follow the real pane shell, wait for live shells, and mark what they installed (D1b-6c-1)
* fix(wsl-mover): history installs wait until live shells can be checked (D1b-6c-1)
* feat(relay): log what the sticky slot spends its hold on, per pooled connection
* fix(relay): time every sticky round trip at the PostgreSQL client, pool wait apart
* docs(relay): say which round trips the sticky slot tally covers
* fix(agent-launch): find a repo's project the way the window does
The host now also takes a project named after the repo as that repo's project, as the window
does, so both pick the same project row and runtime even when a repo was moved out of the
project named after it.
* refactor(agent-launch): share one repo-to-project match with the window
The host's agent-launch lookup and the window's launch preflight now call one projectMatchesRepo
instead of each writing the match out. No behaviour change on either side.
* feat(relay): watch a reserve cell and flip it back on the first step 5 stop rule
A manual, inert-until-dispatched workflow samples one reserve cell every minute for up to
300 minutes against the step 5 §9 stop rules, logs one JSON line per sample, and on the
first trip (or N unreadable samples in a row) runs the flag tool's own admitMode=db write
under the flag workflow's concurrency group. dry-run logs the flag tool's plan instead.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): keep the reserve watch's flip back out of the flag group, and baseline 503s before the flip
- The flip back holds no concurrency group: GitHub keeps one pending run per group and a
newer one cancels it. The flag tool's CAS orders it instead; a generation conflict is
re-read and retried (5 tries, backoff) while the switch file still says reserve.
- The 503 background comes from the 10 minutes before a required flipped-at, or an explicit
value; neither is refused.
- A failed or cancelled write-mode watch flips back too. An unreachable cell fails with the
exact break-glass inputs.
- Missing 503 data (empty series, unpublished minute, unparseable time) is unverifiable.
- Seat-behind-row key adds the director instance and cell; six segments share one command.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): read the reserve watch's 503 publishing front from every response code
A quiet fleet has no 503 series, so the watch read it as unverifiable and flipped a healthy
cell back within minutes. The 503 read now groups request_count by response code in one
query; the newest minute any code has a point for is how far the series is published, so a
503 minute at or before it with no point is zero and minutes past it are left out.
Also: the flip-back job's steps run on always(), flipped-at ages against the watch's start
in later segments, a pending pre-flip background no longer adds to the unverifiable streak,
the flip back retries runtime-status before calling for break glass, and the concurrency
rationale lives once in the docs.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): state what a cancel during the flip-back lease wait can leave, and the 503 publish-skew assumption
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): fail a segment closed on a corrupt watch state file, and keep no checkout token on disk
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): alarm on a failed flip back, refuse a watch under a rollout lease, wait for reserve to apply, and control the duplicate read
- A flip back whose write or plan never finished (no ID token, a failed login, a silent cell)
raises an ::error:: annotation and a FLIP BACK FAILED summary, as the clock-skew monitor does.
- The first segment reads the Cloud SQL rollout lease through the lease action's own reader and
refuses to start while any run holds it, or when it cannot be read.
- A cell not yet reporting reserve is waited on for up to 10 minutes, judging nothing, before the
watch ends refused without a write.
- The duplicate-seat read takes the per-minute ledger summary in the same query and window; with
no fresh summary in it the sample is unverifiable, not zero duplicates.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): page on a refused reserve watch or a failed flip back
A refused watch can leave a cell in reserve and unwatched, the same as a flip back that did not
land. Both now raise an escaped ::error:: annotation through one `alarm` command, and in write
mode write orca_relay_reserve_watch_alarm with gcloud logging write. A new log-based metric and
alert policy page on any such line, per cell, through relay_alert_notification_channels, and the
deploy identity gets roles/logging.logWriter in production to write it.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): name the log-writer risk, check the alarm's project, and say a failed login cannot page
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): note a force-cancel can skip the reserve-watch flip-back write
The drag preview's set-and-restore of the active tab per group now goes
through previewDragActiveTab, restoreDragActivationSnapshot and
restoreSourceGroupActiveTabAfterDrop instead of setState from a component.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Worktree removal, purge, rename, activation wake and SSH target cleanup now
build their tab/group/pane layout changes with named transitions in
tabs/worktree-layout-lifecycle.ts, folded into the same single store update.
Project removal drops its layout writes: purgeWorktreeTerminalState, run just
before in the same call, already cleared every one of them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(terminal): park and quit capture keeps the stored pane layout
The capture that runs on quit, sleep and park rebuilt the whole pane tree
from the DOM. The tab's stored layout already follows every pane change, so
the capture now writes only what it alone knows onto that layout: the
scrollback buffers, the pruned scrollback refs, and the live PTY bindings
with the focus repair that goes with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): keep a stored leaf's scrollback and binding while its pane is unmounted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- ko Worktree Jump Palette: render "actions" as 동작 so it is distinct from
the tasks entry (작업) in the same list.
- zh DevTools cookie instruction: clarify the filter phrase to
`任意包含 remains 的请求`, keeping the literal `remains` request name.
Scan 2026-10-14: fill target-locale coverage for PRs merged since the
2026-10-13 scan (#28486). Adds the missing keys (es 98, fr/ja/ko/zh 125
each), updates the two changed WorktreeJumpPalette strings, and fixes
pre-existing mistranslations: the ZCode missing-TUI notice (English in
all five locales), fr settings search keywords (procuration/indigene),
zh workspace-cleanup browse MT, 6 zh keys that still used Latin "Agent",
and the zh AccountsPane credential-help cluster (leftover English and a
mangled __Host-console_session token).
Leaves the 11 pinned provider-credit-balance keys plus the 3 pinned Chat
UI keys as English fallback by design. Records the scan in
config/update-translation.txt.
* fix(ssh): the SSH password dialog no longer closes itself and comes back
A host that drops an unanswered login no longer replaces the open prompt
with a new one, a background dial no longer replaces a connection whose
reconnect is asking the user, one Cancel closes every open prompt for that
host, and a paired client's automatic connect stays automatic.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): a typed password its reconnect has yet to use is not replaced by a background dial
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
* Avoid unused module imports and trim cosmetic test coverage
* Capture hidden popup frames and attach CDP before guest creation
* Overlap relay test projects within the existing worker budget
* Consolidate native preparation and add an opt-in transform cache comparison
* Release inherited debugger sockets during final E2E teardown
* Refresh unit scheduling weights from all five qualified ARM shards
* Avoid retaining unasserted RPC call history in the delivery model
* Use the supported unconditional background wait syntax
* Overlap unconditional output upload with native preparation
* Verify relay integration stays enabled for ordinary unit callers
* fix(terminal): bring each pane back after a terminal service restart when it is shown
After Settings -> "Restart terminal service", every pane that was open kept its
dead process: typing did nothing and agents never resumed until Orca relaunched.
A restart-replaced exit now arms the pane: the visible pane spawns its
replacement at once, any other pane when it is next shown, through the same
fresh spawn and cold-restore agent resume the hibernation wake uses, so old
output stays above the new prompt. The exit keeps the agent status the resume
reads, and clears the dead binding so the replacement binds as a first spawn.
Local spawns wait for an in-flight restart so they reach the new daemon.
Claude-Session: p2-headless
* fix(terminal): resume from the live recovery record; skip a pane another flow rebound
A restart exit is a process death to main's agent-status store, which clears the
pane's row before the window sees the exit, so keeping the window's copy changed
nothing. The resume reads the live recovery record every agent turn already
writes, as a relaunch after a crash does. The replacement also stands down when
the pane's stored binding already names another process.
Claude-Session: p2-headless
* fix(terminal): send every restart exit down one path
A finished-turn agent pane took the hibernation wake, which gives up when its
record changes before the pane is shown. Every restart exit now arms the
replacement directly.
Claude-Session: p2-headless
Ten create sites now mint the new tab's id (or browser page id), pass it to
the create action, and key every follow-up (select, focus, tab order, group
focus, remote page handle) off that id instead of the returned record.
Creates stay synchronous, so the follow-ups still run immediately.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A workspace with no groups got a random root group id from the window. It
now gets the name-based id the shared loader already mints for that
workspace's group, so main can derive the same id without the window.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(wsl-mover): a moved pane reopens in its last folder on the WSL host (D1b-6b)
* fix(wsl-mover): consume a restore hint only when its pane spawns, scope it to its worktree, retry failed installs (D1b-6b)
* feat(orcad-migration): a capability-gated blob channel beside scrollback, for the WSL mover's new data (D1b-6a)
* fix(orcad-migration): the commit names its blob set, prune clears leaked blobs, reserved names refused (D1b-6a)
* feat(relay): split step-5 shadow compare and dry-run logs per cell
The 24 h per-cell shadow gate needs per-cell sticky-compare counts, per-cell
unexplained counts, and dry-run `full` refusals while the map showed room.
Both summary lines gain additive per-cell fields; existing fields are unchanged.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count a cell-mismatch on both cells and bound per-cell maps by the fleet
- countByCell (was cells) counts a compare on every cell it involves: the
database's cell, every seat's cell when there is no row, and both cells
for a cell-mismatch. Drop the 64-cell cap and cellsOverflow.
- Add ReservePlacer.free and use it for the dry-run free share.
- Rename the room bound to RESERVE_DRY_RUN_ROOM_BOUND.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count a duplicate-seat compare on every seat cell
An unexplained duplicate seat counted only on the database's cell, so a cell
whose stale feed kept a second seat could pass its own step-5 check. Count it
once on every seat cell, the same as a cell-mismatch.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count explained cell-mismatch and duplicate-seat on the database cell only
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): key per-cell counts on prototype-free records
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): explain a duplicate seat left by a just-superseded half-open control
When the database names the newest seat exactly, every other seat has a
strictly lower epoch, and the newest seat joined within the cell's silence
close (timeout + one ping interval + the map lag bound), the older seat is
a half-open control awaiting its 75 s pong-silence close, not a fault.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): bound the superseded duplicate-seat rule on the move's age
A same-epoch rejoin resets joinedAt, so a stale older seat could stay
explained forever; the director-clock sighting of the epoch moves only
when the epoch rises.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): leave a superseded duplicate seat unexplained without an epoch sighting
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): explain a superseded duplicate seat only from a prior sighting
compare() recorded the epoch before classifying, so a first-ever sighting
(restart or eviction) read as a fresh move. Classify first and take the
move's age from the record that existed before this compare.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): also bound a superseded duplicate seat on its join age
A long-ago lower-epoch sighting dates the move to this compare, so the
current seat's join age bounds it too.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count an ageless superseded duplicate seat as its own class
A superseded-shaped duplicate-seat with no prior sighting of the host (the
first compare since a restart) has no move age, so it is now
`superseded-unknown-age` (explained: false) rather than a plain duplicate.
The count line also carries `uptimeMs`, so the gate can discount that
class in an instance's first 100 s.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): limit superseded-unknown-age to an instance's first 100 s
Flushes are lazy, so a window has no upper bound and an uptime field on the
count line cannot date it. The compare now records when it started and
emits superseded-unknown-age only within the superseded close bound of
that; later, a missing sighting means another instance served the host or
the sighting was evicted, so it is a plain unexplained duplicate-seat.
Drops the uptimeMs field.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): say where a missing sighting comes from after the first 100 s
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* perf(ci): reduce test overhead and reuse expensive build work
* test(ci): keep worker startup outside failure-case deadlines
* test: isolate push fixture advisory locks
* test(ci): preserve fixed drain retry allowance
* Align CI contracts with current triggers and guide content
* Stabilize consolidated reveal scenarios and retain behavioral CI contracts
* Fix upstream lint blockers while retaining behavior and assertions
* Classify global Jira preferences in the WSL migration inventory
* Align Jira test fixtures with startup and build safety checks
* Include package-test routing dependency in CI checkouts and fixtures
* Keep native stream classes intact in transcript dependency tests
* fix(terminal): keep a hibernating pane's resume record through its stop
The agent dies before its shell, so the shell's command-finished mark read
as the agent exiting and deleted the record the hibernation had just written.
* fix(automations): sleep a finished run's terminal instead of closing it
Closing it left the run's own workspace empty, so opening it later showed a
blank chat or shell instead of the run's conversation.
Fixes#28645
* feat(chat): Resume in New CLI for OpenCode and OMP chats
OpenCode's and OMP's CLIs have no copy flag, but their chat protocol copies a
conversation (ACP session/fork). For a fork of one of their chats, the chat's
host now has a short-lived agent, launched exactly as the chat's own, copy the
conversation; the terminal then opens the copy with a plain resume. The chat
keeps its own conversation, so the CLI is never a second writer on it.
The copy travels on the existing aiVault.prepareSessionResume fork request as
an optional forkedSessionId; a host that predates it answers without one, and
the client asks for an Orca update instead of resuming the original.
* fix(chat): open an OpenCode 2.x chat's CLI copy with opencode, not opencode2
The copy kept the row's agent, so Resume in New CLI ran `opencode2 --standalone --session <copy>`,
which OpenCode 2.x doesn't install. The chat's host now answers the copy with the agent whose CLI
opens it: the chat's own.
* fix(chat): type the copied conversation's agent as a history agent
* fix: preserve independent chat copies through CLI launch
* test: keep only the tests that pin user-visible behavior
* fix(chat): validate OpenCode copies on their execution host
* test(chat): expect copied OMP forks after main integration