Covers the wiring the parser tests cannot reach: the frame is visible at mount
and gone by the time the host's model list arrives, so only the cached screen
can drive the second resolve. Fails against a listener that merely replaces the
models.
Live capture at 60 columns: a Haiku session prints a bare `Haiku 4.5` row with
its billing wrapped to the next line. No middot, no effort suffix — nothing
marks it as the model, so it reported nothing. Claude always closes the frame
with the working directory and prints at most the descriptor plus a wrapped
billing line above it, so fall back to walking up from there when no row
carries descriptor metadata. The height bound is what keeps the walk from
climbing into the welcome art.
The scraper matched the static seed while the picker lists what #12369
discovers from the host CLI, so the two spoke different id spaces. On a current
CLI `list_models` returns `opus[1m]`, `sonnet`, `sonnet[1m]`, `fable` and
`haiku` — no plain `opus` — while the seed only knows families. Reporting
`opus` therefore selected a row the picker had to invent, dropping the host's
own effort and fast-mode descriptors with it. Locating the model row correctly
made this the normal case rather than a rarity, since the scrape now succeeds.
Resolve against the discovered list first, falling back to the seed for aliases
a host no longer lists and to the raw name for genuinely custom models. Matching
requires the family to lead as a whole word and the label's remaining tokens to
appear in order, so `Opus 5 (1M context)` picks `opus[1m]`, plain `Sonnet 5`
keeps `sonnet` instead of being captured by the 1M-context row, and
`opus-internal-v3` stays custom. Most specific label wins.
The hook keeps the screen that parsed so a discovery landing after the first
read re-resolves it, rather than stranding a family id once the frame has
scrolled out of the buffer.
* fix(terminal): stop transient probe blips from erroring restored panes (STA-3536)
terminal_pane_owner_unverified fired for every restored pane whenever one
liveness probe answer went missing: a cold-start daemon draining an attach
stampede misses the 2s getSize deadline, and a wedged superseded daemon
(protocol upgrades leave them running) turns every unmapped fan-out probe
null forever.
- probePtyOwners now skips legacy daemons whose startup inventory listing
succeeded: fresh sessions never route to them, so they provably don't own
an unmapped id and one wedged zombie can't poison every pane's verdict.
- attachStablePaneOwner retries the probe over a short backoff ladder before
surfacing unverified, so a single missed deadline resolves to a verdict.
- The renderer replaces the raw error code with actionable copy.
* fix(terminal): stop retrying definitive owner probes
* fix(terminal): recover live panes after renderer restart
* refactor(terminal): share owner resolution abort guard
Post-merge review of #12790 demonstrated a real leak: the resume-from-pause
pardon cleared banked misses outright, so a host whose sweep stalls once every
three ticks reset the budget forever and a dead socket was never reaped. The
reviewer ran 300 sweeps against a permanently dead peer with a >1.5x gap every
third tick and observed zero terminate() calls.
Pre-#12790 that required a stall on *every* tick; the counter widened the
pathological window 3x, and the failure mode is permanent non-reaping — the
MAX_WS_CONNECTIONS leak the reaper exists to prevent.
A stalled tick now charges no miss, which is all the original rationale needed
(the client had no chance to answer that probe), but no longer forgives the
misses already banked. A live client still clears its own count by answering
the probe that is still sent on the stalled tick.
The tolerance test's pause case is rewritten to assert the new contract rather
than the old forgive-everything one, and a new test pins the leak directly: a
host stalling every third tick must still reap a dead socket.
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
Stacked on the #12793 revert. Widens the passive-identity set from
{inactive} back to {active, done, inactive}, so the PR/check glyph
returns to the left status lane for workspaces that are actively being
worked, not just idle ones.
Tradeoff, deliberate: #12658 was not purely a regression. It also fixed
#8813, where an active workspace with branch identity and no PR showed
the grey branch glyph instead of the emerald Active dot. This revert
reintroduces that, and removes its e2e guard.
The left lane holds one glyph, so activity, branch identity, and review
status cannot all be shown. This picks review status.
#12793 fixed PR status being hidden by workspace activity by relocating
it: prDisplay was dropped from WorktreeCardStatusSlot and re-rendered as
WorktreeCardReviewStatus in the title-row indicator group, at the right
edge of the card.
Reverting restores the review glyph to the left status lane. Because
#12658 still narrows the passive-identity set to {inactive}, this alone
brings PR status back only for inactive workspaces; reverting #12658
widens it to active and done.
No behavior change for branch identity, and #8813's guard stays intact.
The paired-runtime WS heartbeat terminated a client after a single unanswered
15s ping. One missed pong is UNKNOWN, not proof the peer is gone: a cellular or
Tailscale blackhole, or a stalled TCP retransmit, routinely swallows one pong
from a peer that is still there. Users on flaky paths saw constant drops, each
costing a full redial plus E2EE re-handshake and subscription replay.
Reap now needs MISSED_PROBE_LIMIT (3) consecutive unanswered probes, counted per
socket rather than timed. Any proof of life -- pong or any inbound frame -- clears
the count, as does a resume from a server-loop pause, since a gap the client was
never given a chance to answer must not top up its budget. Missed sweeps still
re-probe, so a recovered path proves itself on the next tick.
Three matches the liveness budgets already in the product: the web client gives
45s (25s idle + 20s probe grace) and the relay control gives 75s. The paired
transport's single miss was the outlier.
Also gives the web client's redial the one-sided jitter the shared-control path
already had, so a fleet dropped by one shared blip does not re-dial in lockstep;
the helper is extracted to src/shared/reconnect-jitter.ts and shared by both.
STA-3320, #12327
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
The scraper assumed the model descriptor sits within three rows of the
`Claude Code vX` line. It does not: Claude prints it near the bottom of the
startup frame with the welcome art and release-notes panel in between — eight
rows down at 100 columns. Narrow panes degrade the frame further, dropping the
version from the title row entirely below ~70 columns and wrapping the billing
tail onto its own row. Any one of those made the scrape return null, so the
model picker showed no current selection at all.
Search the frame from its bottom border upward for the row carrying model
metadata, read only the leftmost frame cell so release-notes prose can never
win, accept the frame corner as header proof when the version is gone, and
tolerate the effort suffix being elided to an ellipsis. Catalog families now
match as a leading word, which both survives the resolved-name suffix
("Opus 5 (1M context)") and keeps custom slugs like company/my-haiku-v2 from
being claimed as haiku; an unrecognized name is reported as a custom model.
Fixtures are real: captured from a live claude 2.1.220 by replaying the PTY
bytes through @xterm/headless and serializing exactly as TerminalPane does.
* fix(mobile): keep the cached transcript visible while reconnecting
A manual retry closes the client and opens a fresh one, so the chat session
hook saw a new client under an unchanged identity, dropped its settled read,
and handed out an empty list — the transcript collapsed to a full-screen
spinner until the swapped client's snapshot landed.
Hold the last settled list per identity (captured post-commit) and keep
rendering it while the re-read is in flight. `transcriptLoading` still gates
consumers that decide from an empty transcript, so the launch-draft seed is
unaffected. The held list is keyed by a new `sourceIdentity` (host/workspace)
in addition to agent/session/transcript, so it can never serve another
source's messages.
Refs STA-3333.
* test(mobile): assert the whole reconnect window, not just its first frame
The re-subscribe lands a commit after the first render of the swap, so a
regression that cleared the held list there left frame 0 green and still
blanked the transcript. Verified: clearing the cache in the subscribe
cleanup now fails this test, where before only the view-toggle test caught it.
* fix(mobile): don't derive a tappable ask card from the held transcript
The cache this PR adds keeps the previous list rendered while a swapped
client re-reads. useMobileNativeChatPrompts was the one consumer reading
`messages` without honouring `transcriptLoading`, so an ask answered on
the terminal resurrected as a live, tappable card during that window.
Gating on `transcriptLoading` is exactly base behaviour: `setRead` only
ever stores 'ready'/'error', so status==='loading' implied an empty list
before this PR. The live `askFromStatus` path is untouched.
* chore: keep merge formatting scoped
`orca serve` publishes a ready graph under HEADLESS_RUNTIME_WINDOW_ID with no
BrowserWindow behind it. `shouldCreateInBackground` only degraded when the
create was renderer-backed, so any focus-requested create fell through to
getAuthoritativeWindow() and threw "No renderer window available" — leaving
`terminal create --focus` with no workaround on a remote server (#10333).
With a worktree selector and no renderer window, a background spawn is the only
usable path, so collapse the renderer-backed window check into a plain
"no window" check. That is the existing rendererBacked clause plus exactly the
missing focus case, and it drops the confusing `rendererWindow === null`
indirection (rendererWindow is already gated on rendererBacked).
Focus is not lost by the degrade: the spawned pane is still published to the
session-tab model and revealed with `activate: true`, which is how a paired
client learns about it. Mirrors the in-tree precedent in
runCreateMobileSessionTerminal.
Headed hosts are unaffected — the clause only fires when no window exists.
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(file-explorer): sort numbered file names naturally
The File Explorer compared names with bare localeCompare, so numbered
files listed 100, 200 before 99. Hoist the numeric collator Source
Control file rows already use (#10850) into src/shared and apply it to
the local and runtime directory listings, the name-filtered view, and
Source Control directory nodes, which were inconsistent with the file
rows one line below (#11426).
* fix(file-explorer): natural sort on SSH funnels, relay, and pickers
Adversarial-review round 1 rework:
- Both readDir funnels short-circuited to the SSH filesystem provider
before the patched sort, so SSH workspaces kept lexicographic order;
re-sort locally after the provider returns (the remote relay may be an
older build), and fix the relay's own comparator for relay-native
consumers.
- sortDirEntries (shared, unit-tested) owns the directories-first +
natural-order listing contract used by every funnel.
- compareFileNames breaks numeric-collation ties ('2' vs '02') by code
units so sibling order stays total instead of readdir order, and pins
the collator locale to 'en' so every host produces one order.
- The SSH folder browser and runtime server dir picker now match the
Explorer they browse into.
- Ordering pinned by tests at the relay, source-control tree, and shared
helper.
* fix(mobile): natural sort in the mobile file explorer
Mobile re-sorted host readDir results with bare localeCompare, undoing
the host funnel's natural order (round-2 review). Reuse the shared
comparator and pin the order in the mobile suite.
* fix(file-explorer): natural sort at the renderer choke point and remaining ties
Round-3 review: the remote-runtime RPC and paired-web routes return the
host's order verbatim, so re-sort in readFileExplorerDirectory where
every desktop route converges; pin the SSH funnel with a handler-level
test; and route Source Control path compares through compareFileNames so
numeric-collation ties share one total order with the Explorer.
* docs(file-name-sort): state the real perf baseline in the hoist comment
* refactor(source-control): drop the dead collator export; pin the test oracle locale
* fix(file-listings): cover remaining natural-sort surfaces
Add a new census module that tracks pending and retained OSC sequences
across all active PTY output processors. Each processor registers a gauge
at creation and unregisters it on dispose, detach, or destroy — this
prevents retained gauges from inflating later heap high-water profiles
and allows the memory profiler to detect stalled processors as a sign of
leaks.
* Display SSH worktrees immediately using persisted metadata
Users can now see known worktrees for SSH hosts without waiting for the
provider connection to establish. Worktrees are fetched from local metadata
and displayed as non-authoritative, then merged without replacing richer
live data once the provider becomes available.
* Show SSH folder workspaces immediately via persisted metadata
Add safeguards for metadata fallback: track authoritatively removed
worktrees per host to prevent resurrection, position new rows within
the host block to avoid jumping on authoritative scan arrival, and
preserve co-owner detection status during merge. Coalesce concurrent
metadata fetches to dedupe overlapping queries.
* Reorder source control to show staged changes first by default
Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.
* Drop source control group order user preference
Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.
* Reorder source control to show changes before staged
Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
- Replace 'Send answer' with 'Submit' for clarity and consistency
- Update all locale translations (en, es, ja, ko, zh)
- Remove fixed button width and add whitespace-nowrap for flexible sizing
- Update component and test references
* Add branch line total chip to source control header
Display the total lines added and removed across a branch from its fork point, measured via `git diff <mergeBase>`. Only computed when the chip is visible (request gate on merge base OID), with 500ms soft deadline to protect status latency and 15s hard timeout. Deduplicated across concurrent pollers and cached alongside line stats. Omitted on failure — always shows exact or nothing, never a partial estimate. Updates throughout the stack: native git status, relay, renderer store/API, and UI components.
* Pin branch line total to app locale
Format line counts using the app's configured locale instead of the system
locale, ensuring consistent cross-platform display and test reliability.
* test: wait for coalescer joins instead of fixed sleep
Hold the diff until the second status pass actually takes the
branch-total coalescer lease instead of using a fixed 400ms sleep.
Fixes timing-dependent flakiness on slow machines.
* fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset
Fixes#9358 and #9941.
cmd.exe expands %VAR:~n,m% at parse time. When GROK_HOME is unset (default
outside Orca terminals), the generated length/trailing-backslash guards
became a syntax error and every Grok hook event failed with exit 255.
- Skip substring work when GROK_HOME is undefined (if defined + goto)
- Replace if "%x:~-1%"=="\" (itself a quote-parser bug) with findstr
- Extract Windows script builder; add template + spawn tests
* fix(windows): harden grok-hook GROK_HOME guards and tests
Address review on #11782:
- Inject grokHome via buildWindowsAgentHookPostCommand extra form lines
(no fragile string replace of the shared payload line)
- Spawn tests delete GROK_HOME and keep PORT/TOKEN/PANE_KEY set so the
GROK_HOME path actually runs before curl
* fix(windows): cover Grok hook home boundaries
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
PR 9501 shipped real-home routing for the host system default, and the
env override that could turn it back off was never a shipped control. The
managed-account half of the shared runtime mirror has been unreachable
since: every host account routes to its own self-contained CODEX_HOME
before that code runs.
Delete the flag module and its env plumbing plus the managed branch of
syncForCurrentSelection and the six helpers only it called. The three
lanes that still use the shared mirror -- Windows, a custom CODEX_HOME,
and a hook-lane gate that reports unusable -- are untouched, as are every
legacy migration and the WSL read-back helpers.
* feat(browser): add hard reload option and shortcut hints to reload button
Add a tooltip to the browser reload button showing the reload shortcut.
Add a right-click context menu with Reload and Hard Reload options.
Add localized labels for Hard Reload across EN, ZH, JA, KO, ES.
* fix(browser): add aria-labels to reload buttons
* feat(browser): make reload button contextual and extract action logic
- Button label now reflects actual action: Stop when loading, Retry on failure, Reload when idle
- Extract reload intent resolution into reusable browser-reload-action module with tests
- Add keyboard support (Enter/Space) for the reload button
- Simplify remote page reload to tooltip-only (no ignore-cache RPC for remote pages)
- Add "Stop" translations for all supported languages
* fix(browser): exhaust reload intent switch for type-aware lint
Replace the default branch with an explicit reload case so oxlint
switch-exhaustiveness-check accepts BrowserReloadIntent.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Refactor GitHub work-item mutations onto a shared optimistic coordinator
- Extract PR/issue status, assignee, reviewer, and merge/auto-merge mutations
out of TaskPage cell components into a registry-backed
begin/confirm/rollback pipeline (task-page-github-work-item-mutation-*),
so soft-hide, sticky filter-membership, and quiet revalidation behave
consistently across all mutation types instead of each cell re-implementing
optimistic update/rollback/toast logic.
- Add quiet revalidation (no filter skeleton, no page blanking) and soft-hide
handling so a row that exits the active filter (e.g. closing an issue under
`is:open`) stays hidden without a jarring list reflow.
- Restyle the GitHub task table: opaque sticky ID/Title cells, distinct header
fill, accent hover, and tighter row/toolbar chrome to fix background bleed
and muddy contrast in the scrolled table.
* Fix quiet-revalidate cancellation and sticky-hide scoping in TaskPage
- Replace per-render `cancelled` flag with a ref that only flips on
true unmount, so a nonce-triggered re-render no longer strands the
shared quietState's trailing/backoff bookkeeping mid-flight.
- Fix backoff index to use max lag attempts instead of lagging-key
count, matching processTaskPageQuietRevalidateSettle so several
single-lag items can't jump the delay tier.
- Scope sticky-hide retention in materializeTaskPageItemList to the
originating query key, preventing non-membership confirms (e.g.
auto-merge) from lingering as stale rows across refetches.
* Fix is:draft filter to soft-hide non-draft PRs
Previously state was forced to 'open' for is:draft queries, so a PR
that stopped being a draft still passed the state check and stayed
visible. Add an explicit draft check to soft-hide it.
* Improve GitHub work-item mutations with scoped quiet revalidation
Prevent race conditions and stale data by tracking quiet run ownership,
validating scope changes with generations, and blocking overlapping mutations
with pre-flight checks. Extract quiet state management into a dedicated module
with improved authority clearing and network retry logic.
Reorganize the host and project filters to share a unified single-row design
(label left, value right) with detailed selection moved to nested panels. Group
both filters under a "Show" section label to keep the parent menu flat. Extract
project-filter search logic into SidebarProjectFilterPanel with explicit focus
and keyboard-handling tests.
* fix(worktree-watcher): surface external push -u through the git-common watch
An external-shell 'git push -u' writes only the common .git/config (plus
refs/remotes/<remote>/<branch>), both invisible to the git-common event
filter, so the Checks panel stayed on 'No upstream configured' until the
renderer safety poll. Classify the common config and remote-tracking refs
as status-tier signals, poll config alongside the other primary-checkout
metadata files, and keep FETCH_HEAD/reflog/ref-lock churn ignored.
* fix(worktree-watcher): refresh after subsequent pushes
* feat(editor): add markdown table structure controls
* fix(editor): scope table context actions to cells
* Replace table toolbar with context-aware overlay controls
Replace the fixed table toolbar with context-sensitive overlay controls that position themselves around the active table, adding support for direct row/column insertion and full table deletion. This approach is less intrusive and supports click-targeted actions via coordinate-based cell resolution. Enhance structural safety by preventing header removal and ensuring tables never collapse below a single cell, deleting instead when the final row or column is removed. Harden the context-menu query with a 120ms timeout to keep the native menu responsive even if the renderer hangs.
* Replace markdown table context query with IPC coordination
Capture table cell targets on pointerdown and report via IPC channel
instead of executing JavaScript on context-menu events. Eliminates
120ms query timeout and unavailability race conditions. Header cells
now disable incompatible row-level actions.
* fix(editor): make table column rebalancing atomic with insertion
- Refactor rebalanceAddedColumn to mutate the caller's transaction, grouping
insertion and rebalance into a single undo step
- Add validation for cached cell positions that may outlive the document
- Fix table detection to use isInTable() instead of isActive('table')
- Correct z-index layering to respect menu stacking context
- Fix cleanup of stale animation frames and pending pointer state
---------
Co-authored-by: rainL <WYK15@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Validate auto-generated PR details; abort with clear errors
Add fail-closed validation for generated review fields to ensure empty
bodies and generation failures prevent unintended auto-submission, with
user-visible error messages. Preserves current base ref since intent
auto-submits without confirmation.
* Add translations for PR generation error messages
#12746 only delivers pty.data after an authenticated openClient grant.
Update the macOS release harness to use a credentialed --connect client
and open a legacy session-owner so the gate still exercises watcher
isolation without timing out on the initial PTY echo.
Add targeted recovery for rejected PTY source frames instead of
terminating the relay channel. Classify rejection reasons (malformed,
generation mismatch, range invalid) and attempt recovery based on the
rejection type. Implement admission control at publication time to ensure
frames aren't delivered after ownership changes. Bound recovery attempts
and retry with backoff to prevent exhaustion. Diagnose and log rejection
reasons to aid debugging.
An owner-capable `pty.openClient` had two failure modes that presented as something else.
If the relay still held an owner record but the request carried no matching resume proof, admission fell through to a SUBSCRIBER grant — a success-shaped response the client cannot use, which it then rejected as "did not grant an authenticated PTY session owner". And if the relay had forgotten the record the client named, admission threw a stale-recovery error, which the client answered by deleting its own recovery row — `clientInstanceId` included — and reopening. Two round trips, and the identity that lets it resume that target at all went with the deletion.
Now every owner grant carries a required `resumed` flag, a forgotten record mints a fresh claim in one round trip, a held claim returns one of three coded refusals, duplicate opens on one connection are rejected even when identical, and an attached-holder refusal becomes a typed error routed through the terminal-relay-error callback instead of feeding redeploy backoff a link that is working fine.
Independent review caught two regressions in the first attempt, both now fixed and both with tests that fail without them:
**A backpressure teardown could take a live owner's session.** The safety argument was that a record only becomes `disconnected` from an observed peer close — but two of the six paths there are capacity paths, where the relay destroys the client's socket itself because its lane queue filled. That is the signature of a client that is ALIVE but not draining fast enough. Demonstrated: the real owner is torn down for backpressure, a rival is granted ownership 270ms into a nominal 30s grace, and the owner's later reconnect with a valid resume proof is refused permanently, backoff cleared, no retry. Closes now carry a cause (`peer-closed` | `local`, defaulting to `local`, which only ever widens a grace), and the floor applies only to closes the transport actually observed on the peer's side. Capacity teardowns, decode faults and sink failures keep the default.
**A client's own zombie connection blocked it permanently.** Only `SshRelaySession` ever requests owner, and every endpoint-credential client shares one principal — so in a normal single-app deployment an `active` incumbent refusing you is almost always your own half-open connection the relay never saw close. That was refused as terminal, where main recovered on bounded backoff once keepalive noticed. The refusal already held both client identities; a match is now a distinct transient refusal that falls through to relay-lost backoff, restoring that recovery. A genuinely different client is still blocked.
Also: each retry deadline now starts when its own phase begins, instead of both being computed at entry where a slow first phase could leave the second with zero attempts.
Fixes STA-3365.
Per-target SSH teardown awaited `removeAllForwards` BEFORE anything marked the lease detached, so a slow forward close let the final store flush snapshot while leases still said `attached` — and the later durable write was rejected because persistence had already finalized. On the next launch those leases described a state that never existed.
`beginSshShutdown()` now performs every in-memory transition synchronously before returning, and the quit path calls it immediately before `store.flushAsync()` with no await between. The whole drain shares one deadline that REPORTS unfinished `{targetId, phase}` rather than concluding anything about it, and `waitForSystemSshForwardStop` gained a post-SIGKILL bound.
Nothing here destroys a session. `detached` means this app let go of the lease, not that the shell died — the pre-pass exists precisely so still-running PTYs are recorded as detached-but-alive instead of being lost to an `attached` snapshot. Review confirmed every reader honors that: reattach enumeration and persistence restore filter only `terminated`/`expired`, lease normalization has no age-based expiry, and attempt exhaustion leaves a lease alone. The drain deadline's only consumers are a warning log and a join that discards the value — nothing reads it as "gone".
Review also caught a defect the refactor introduced, now fixed: making the pre-pass synchronous meant a throw from `beginShutdownDetach` — via `webContents.send` on a renderer that quit had already destroyed — escaped the non-async `will-quit` listener and skipped `killAllPty()`, the watchers, `store.flushAsync()`, the teardown barrier and `app.quit()`. That would have lost the exact snapshot this PR exists to make correct. Each call is now wrapped per session, collecting errors and continuing. Proven: the test throws from the first of two sessions and fails without the fix with "Object has been destroyed".
A second test could only fail via timeout rather than assertion; the ordering is corrected so removing the post-SIGKILL bound now fails in 5ms with a clean assertion instead of a 5s timeout.
Rebased onto main and verified independent of #12673 (zero references to its owner-admission changes), which is being reworked separately. Fixes STA-3366.
* Open TaskPage GitHub items directly in workspace composer
Remove background creation indirection. The composer prefills with
issue/pull-request metadata from the GitHub work item.
* Generalize workspace composer tests to support multiple sources
Rename test from task-page-github-composer-boundary to task-page-workspace-composer-boundary. Add test verifying Linear items open directly in the workspace composer, expanding beyond GitHub-specific routing.
* Support issue command automation in workspace composer
- Pass GitHub work items through composer for issue context
- Quick create resolves commands at workspace creation time
- Extract command building and trust logic to dedicated module
* Derive composer hooks by host context for React Doctor
Key loaded hooks and issue commands by execution-host context so
repo/host switches no longer reset derived state in effects (the
static-analysis gate). Also wire SSH-aware workspace targets and
cancel-safe submit settlement for workspace creation.
* Support duplicate repo IDs across hosts in workspace creation
When the same repo exists on multiple hosts (local and SSH), resolve the
workspace creation target to the ready setup on the preferred host instead of
failing closed. Also add resilience to hook checks by clearing the cache on
transient IPC failures.
With a desktop client paired to a remote Orca runtime, terminal panes could report connected, writable, and `terminal.send` returning accepted — yet keystrokes never reached the agent. No error, no banner, no recovery; input silently vanished.
The ticket was really two bugs. The attach half was already fixed by #12589 (subscriber-driven daemon attach), confirmed by reproducing against current main. This fixes the remaining half: a write the host refuses had no way to tell anyone.
A capability-negotiated `WriteUnavailable` opcode carries that refusal back to the client, where it feeds the pane's pre-existing recovery hook. Capability gating matters because decoders reject unknown opcodes on desktop — and, worse, silently drop them on mobile — so the signal is negotiated in the subscribe handshake. Verified per direction: an old host strips the unknown Subscribe key, an old client omits it so the host never emits, and capability cannot be inherited across resubscribe.
Independent review then found the signal was being delivered and discarded: recovery demanded an authoritative liveness answer, and `pty:hasPty` had no `remote:` guard, so a paired pane's id fell through to the LOCAL provider, which returned false, and recovery bailed before remounting. Every test stopped at the transport boundary, so all of them passed while the pane stayed just as stuck. `pty:kill` already had exactly that guard.
The fix makes main answer LESS rather than claim more: `pty:hasPty` now returns unknown for a `remote:` id instead of a fabricated false, because main cannot speak for another host's PTY. The remount is then authorized by positive evidence — the process that owns the PTY stating it refused this specific write over a live negotiated connection — not by inference from silence. Local and app-SSH ids keep the probe, where a false genuinely means the shell died. Nothing is destroyed on this path; the remount rebuilds the renderer over the session it already had.
An end-to-end test now carries a rejected write from the host through to an actual remount, which no prior test did. A surviving mutant was also killed: the legacy-binary capability gate could previously be deleted with nothing turning red.
The reliability gate stays experimental — live paired journeys and mixed installed-release evidence remain uncollected. Fixes STA-2830.
Mixed versions are the normal state of the remote-server feature: users update clients and servers independently. Until now nothing tested that. Every cross-version claim was made by code reading plus unit tests with hand-written old/new shapes — enough to catch design problems, not enough to catch a real skew regression.
This runs the REAL protocol implementations from two builds against each other in one process: the actual host methods and RPC dispatcher on one side, the actual renderer multiplexer on the other, with a transport that reproduces the production asymmetry — each side decodes with its OWN codec and drops frames whose opcode it does not know. A frame survives only if the RECEIVING build understands it, which is what makes this level sufficient without launching two apps. The old side is a genuine checkout extracted from the release tag; the extracted client was confirmed to lack a symbol that exists only on main.
Journey: subscribe, first snapshot, input reaching the process, live output, hide/reveal snapshot, transport drop, resubscribe, input landing again — across old->new, new->old, and a current/current control. Every step ends on an observed-state barrier; no sleeps. The oracle asserts the recorded step list, the exact 16-frame named sequence, negotiated capabilities, the exact input the host wrote to the PTY, rendered content, and zero decoder-rejected frames. A host method the stub lacks is recorded by name and asserted empty, so a harness gap cannot masquerade as a wire break.
Detection is proven per violation shape, and it attributes each to the correct side: an unnegotiated opcode goes red only where a decoder would reject it, a removed published field goes red only where an old client consumes it, and a legal additive field stays green in all three pairings so the harness will not cry wolf on safe changes.
It also documents the three compatibility rules in docs/reference/remote-wire-compatibility.md, linked from AGENTS.md, since they previously existed only as folklore — notably that "decoders reject unknown opcodes" is true for the desktop decoder but NOT for mobile, which silently drops them.
Deliberately scoped: terminal stream only. The session-tab sync channel is not covered, nor agent-session publications, file/Git RPCs, mobile E2EE framing, or the relay transport. Two version points, so a regression introduced and reverted between them is invisible.
CI selection was verified rather than assumed — `vitest list` confirms 0 matches under the shard's exclude and 4 under the dedicated job — because a lane silently running zero tests is precisely how a host-side defect escaped CI earlier in this series. Closes STA-3469.