Git can execute inside a WSL distro against a raw Linux worktree path while Node,
on the Windows side, reads the same files back through Win32. `path.join(
'/home/me/repo/feature', 'src/file.ts')` on win32 produces the drive-relative
`\home\me\repo\feature\src\file.ts`, which resolves against whatever the current
drive happens to be and almost always ENOENTs. The same mis-spelling hits the
drvfs form, where `/mnt/c/repo` should read as `C:\repo`.
Two consequences, both on the Node side only (git already works, because it gets
the Linux path as its cwd and resolves it inside the distro):
- getDiff's unstaged working-tree read missed, `readWorkingTreeFile` mapped ENOENT
to `exists: false`, and an existing file rendered as DELETED in the diff view.
- `readWorktreeDiffStamp` could not find `.git`, so the stamp was null, the settled
diff cache neither hit nor stored, and every diff respawned `git show` - two
`wsl.exe` spawns the cache exists specifically to avoid.
Both now spell the worktree directory for the reading host first, via a new
`resolveWorktreeHostPath` wrapper around the resolver that landed in #17804.
The wrapper exists because `resolveGitMetadataPath` trims: a gitfile payload
carries a trailing newline, but a directory name may legally begin or end with
whitespace on POSIX, so the wrapper keeps the caller's spelling whenever the
resolver only trimmed it. The stamp's opaque `value` still embeds the caller's
original `worktreePath`, so settled-cache identity is byte-identical and no cache
key moves.
`readWorktreeDiffStamp` was already `Promise<WorktreeDiffStamp | null>` with one
caller that treats null as a cache miss, so no new nullability enters the type
system and the resolver's never-null-for-a-non-empty-pointer contract is
untouched. The only unspellable input is an empty worktree path, handled locally
as "not provably unchanged" in the stamp and as a read *failure* (not a proven
deletion) in file-diff.
What changes for users
| Platform | Delta |
|---|---|
| macOS | No change. An absolute POSIX path is returned verbatim, including one whose directory name carries leading or trailing whitespace. |
| Linux | No change. Same reason. |
| Native Windows (no WSL) | No change. A `C:\...` or `\\server\share\...` path is already absolute for win32 and passes through verbatim. |
| Windows + WSL, UNC worktree path (`\\wsl.localhost\Ubuntu\...`) | No change. Already absolute for win32; passes through verbatim. This is today's common case. |
| Windows + WSL, drvfs worktree path (`/mnt/c/repo`) | Fixed. Reads as `C:\repo` instead of the drive-relative `\mnt\c\repo`. Needs no distro name. |
| Windows + WSL, Linux worktree path with a named distro (`/home/me/repo`) | Fixed. Reads as `\\wsl.localhost\Ubuntu\home\me\repo`. The deleted-file misrender goes away and the diff cache starts hitting. |
| Windows, POSIX path, no distro and not a drvfs mount | No change. Passes through verbatim, same ENOENT, same existing fallback. |
| SSH | No change. `runtime-git-diff-commands.ts` and the `git:diff` IPC both route to `provider.getDiff` for a connection, so this local code is never reached. |
| Relay / remote | No change. No RPC param, wire field, stream opcode, or published content is touched; the relay host runs the same local code and gets the same fix. |
| Folder workspace (non-git) | No change. `.git` is absent either way, `resolveGitDir` returns the same fallback, and the stamp stays null exactly as today. |
| GitLab / other providers | Not applicable. No provider-specific or review code is touched. |
What this does NOT do
- It does not fix `resolveGitDir` itself. For a drvfs repo whose worktree Orca
already spells `C:\repo\feature`, the gitfile payload `gitdir: /mnt/c/repo/.git/
worktrees/feature` is still mis-resolved by `path.resolve` to
`C:\mnt\c\repo\.git\...`, so the stamp still returns null in that shape. Separate
change, separate PR; this one neither fixes nor regresses it.
- It does not touch submodule path resolution. `resolveSubmoduleWorktreePath` is
the path-escape guard and has a near-identical twin in the relay; changing it
without escape tests on both is out of scope.
- It does not change `readHeadComponent`'s `commondir` resolution. The relative
`../..` git actually writes takes the identical `path.resolve` branch, and an
absolute POSIX `commondir` under a WSL UNC `gitDir` already resolves correctly
because the UNC root is `\\wsl.localhost\<distro>\`.
- It does not reorder drvfs-before-UNC inside the shared resolver. That changes the
identity of returned strings and needs a real Windows+WSL box.
- It does not add any Git command, option, or version dependency.
Costs and residual risk
- One extra pure function call per diff read. No I/O added or removed on the
unaffected paths.
- Translation still trims. `resolveWorktreeHostPath` preserves whitespace only when
no translation happened; a guest directory named `/home/me/repo ` loses its
trailing space on a Windows reader. Reachable only on win32, where such a name is
not addressable anyway, and the previous behavior for that shape was a
drive-relative miss.
- A relative worktree path (no caller passes one) is now resolved against the
process cwd instead of joined relative to it. Same file in every case except a
relative name that itself ends in whitespace.
- `UNSPELLABLE_WORKING_TREE_READ`'s `exists`/`failed` fields are correct but not
observable today: the stamp is null for the same input, so nothing can be cached
and `reusable` cannot be read back. They are there so the branch stays right if
`loadDiff` ever gains a second caller. The test pins the observable part - that no
read lands on a cwd-relative path.
- Every test here mocks `node:fs/promises` and spoofs `process.platform`. They prove
which path string reaches `stat`/`readFile`, which is the right assertion, but
none of this has executed against a real 9p mount on a Windows+WSL box and this
repo's CI has no such runner.
- Honest framing of the trigger: I could not demonstrate a mainline path that hands
`getDiff` an untranslated POSIX worktree path on Windows today -
`translateWslOutputPaths` UNC-translates worktree paths whenever a distro is
known, `getWslHome` returns the UNC spelling, and `resolveWslRepoWorktreeBasePath`
normalizes a configured Linux base. The drvfs case is the most plausible live one.
Treat this as defense-in-depth that is a strict no-op on every configuration above
except the two marked Fixed.
Verification
- `npx vitest run src/main/git src/shared/git-metadata-path.test.ts` -> 196 files /
2241 tests passed, 2 files and 5 tests skipped. One failure,
`git-admission-storm-measurement.test.ts > reports bounded-concurrency before and
after measurements` (ENOENT scandir on its own temp state dir), is pre-existing
and environmental: it fails identically in isolation and spawns real git children
without touching any changed module.
- `npx vitest run src/main/git/status-diff-settled-cache.test.ts` -> 21/21 (16
pre-existing, 5 new). `npx vitest run src/shared/git-metadata-path.test.ts` ->
25/25 (19 pre-existing, 6 new cases across 3 new tests).
- `npx oxfmt --write` then `npx oxlint` on all five changed files -> clean.
Mutation checks - all eight production substitutions were reverted one at a time
and the suite re-run. Each fails at least one test, and no new test survives its
own mutation:
| Reverted | Failing test |
|---|---|
| file-diff working-tree read -> `worktreePath` | reads the working tree through the host spelling instead of reporting a deletion; invalidates when the working tree file is edited under the host spelling |
| stamp working-tree component -> `worktreePath` | invalidates when the working tree file is edited under the host spelling |
| stamp `.gitmodules` stat -> `worktreePath` | invalidates when .gitmodules appears under the host spelling |
| stamp `resolveGitDir` -> `worktreePath` | stamps through the host spelling so the second read does not respawn git |
| `options` threading at the `readWorktreeDiffStamp` call | stamps through the host spelling...; invalidates when .gitmodules appears... |
| wrapper's untrimmed preservation -> return the resolver's value | keeps whitespace that belongs to the directory name (both cases) |
| `UNSPELLABLE_WORKING_TREE_READ` -> a cwd-relative `readWorkingTreeFile` | reads nothing relative to the cwd when the worktree path has no host spelling |
| stamp's null early return -> `hostWorktreePath ?? worktreePath` | reads nothing relative to the cwd when the worktree path has no host spelling |
The settled-cache tests seed the fake filesystem through the platform-bound `path`
module rather than `path.win32`, so they assert real behavior on a POSIX CI host as
well as on Windows and are not gated on the host platform.
Co-authored-by: Neil <79079362+brennanb2025@users.noreply.github.com>
Untracked line counts, and the untracked share of the branch line total, come
from direct lstat/open calls rather than from git. When git executes inside a
WSL distro the worktree path can be a guest path, which Win32 reads as
drive-relative (`/home/me/repo`) or as a literal `C:\mnt\c\repo`. Every lstat
then fails, countFileAdditions swallows the error into `{}`, the file renders
with no +N, and the branch total silently undercounts it.
Route only those two filesystem reads through resolveWorktreeFilesystemPath,
which translates a guest-rooted path via the shared resolveGitMetadataPath.
Both call sites produce the same string, so the stat-keyed untracked cache
still hits across them. resolveGitMetadataPath itself is not modified, so its
other callers are untouched.
The wrapper translates only when the host is win32 and the path is a guest
spelling: exactly one leading slash, and unchanged by trim(). Each condition
is load-bearing.
- `//wsl.localhost/...` and `//wsl$/...` are UNC spellings that also start with
`/`, and translating one prepends the distro root a second time, so a
worktree that reads fine today would ENOENT on every untracked file. The same
single-leading-slash guard is used, for the same reason, by
resolveWslRepoWorktreeBasePath in src/shared/wsl-paths.ts.
- resolveGitMetadataPath returns `rawPath.trim()`, so a worktree directory name
with leading or trailing whitespace (legal on ext4) would be re-spelt onto a
different directory. Leaving those verbatim keeps them exactly as they read
today.
- Off win32 the resolver is already an identity for these inputs; the platform
check makes the macOS/Linux no-op structural rather than derived.
Per platform:
- Windows + WSL, worktree spelled as a guest path: untracked +N appears and the
branch total includes it.
- Windows + WSL, worktree already spelled `\\wsl.localhost\...`,
`//wsl.localhost/...` or `//wsl$/...`: unchanged, returned verbatim.
- Native Windows: `C:\...` is unchanged. One shape does change: a
`/mnt/<drive>/...` worktree now resolves to `<Drive>:\...` instead of being
passed through to a guaranteed lstat failure. Native Windows does not produce
that spelling, and if it were reached the new result is the correct file.
- macOS / Linux: unchanged; not win32, returned verbatim, whitespace included.
- SSH / relay: unchanged. Remote status runs in the relay, which builds its own
branch-total input and does not pass filesystemWorktreePath.
- Folder workspaces / GitLab: unaffected, no workspace-kind or provider
behavior is touched.
No fail-closed degradation: attachLineStats still returns
`stagedStats !== null && unstagedStats !== null`, and createBranchLineTotalInput
still has no early return. The wrapper returns `string`, never null, so an
unmappable worktree keeps its old spelling and its old (missing) untracked
counts rather than dropping the staged/unstaged counts as well.
The `filesystemWorktreePath` field on computeGitBranchLineTotal is optional and
does not touch the lease key, so the coalescing/cooldown identity is unchanged.
Co-authored-by: Neil <neil@orca.local>
The fetch lock key is the worktree's resolved common Git directory. On a Windows
host two derivations split one repo into several lanes, so sibling fetches on the
same repo race the FETCH_HEAD write the lock exists to serialize.
- Windows aliases \\wsl$ to \\wsl.localhost and folds the distro name and any
drvfs tail case-insensitively, so two spellings of one repo produced two keys.
The finished key now goes through foldWslUncPathCaseInsensitiveParts. This is a
pure function of the finished key, so equal keys stay equal: it can only merge.
- Under a drive-spelled base, git-in-WSL's `/mnt/c/repo/.git` gitfile and
commondir pointers are read by path.resolve as the non-existent
C:\mnt\c\repo\.git. The commondir read then fails and every linked worktree got
its own dead-end key while the main checkout keyed on C:\repo\.git. Such a
pointer now goes through toWindowsWslDrivePath.
- realpath and stat take no AbortSignal, so cancelling a fetch still blocked
behind a hung 9P/UNC lookup. Both are wrapped in waitForPromiseWithSignal. The
rejection keeps today's synthetic AbortError shape, including when the caller
aborts with its own reason, because callers classify on error.name.
- The hand-rolled gitfile regex is replaced by the shared
parseGitdirMarkerPayload, matching git's own read_gitfile_gently.
A WSL UNC base is deliberately excluded from the pointer translation. win32
path.resolve already carries such a base's distro onto a guest-rooted pointer
(\\wsl.localhost\Ubuntu\home\me\wt + /mnt/c/repo/.git ->
\\wsl.localhost\Ubuntu\mnt\c\repo\.git), and a main worktree's `.git` is a
directory with no pointer to translate, so its key stays on that UNC spelling.
Rewriting only the linked worktrees to C:\... would have split one repo across
two lanes - the opposite of the intent. A test now pins that layout.
Direction of every key change, re-derived over a ten-layout matrix that runs both
this code and an emulation of the pre-change derivation under Win32 path rules:
nine layouts either merge or are byte-identical. The fold is merge-only by
construction; the pointer translation's sole delta is C:\mnt\c\X -> C:\X, and
C:\mnt\c\X is derived from bytes that live at C:\X, so it can only join a
worktree to its own common dir. The tenth layout is the one narrowing: the shared
parser accepts `gitdir:` only at offset 0 where the old regex accepted it on any
line, so a `.git` file with a leading blank line falls through to the parent walk
(C:\repo\.git\FETCH_HEAD -> C:\.git\FETCH_HEAD). Nothing can race there - git
2.44 refuses that same file with `fatal: invalid gitfile format`, so no fetch
runs in such a worktree at all. Native Windows repos with no WSL, SSH, relay and
folder workspaces are byte-identical.
hostPath() returns the same node:path submodule Node itself selects, so it is a
no-op on every real host; it exists so the Win32 derivation is testable off
Windows. resolveGitFetchHeadCommand's argument parsing is untouched, and
--git-dir gitfile dereferencing is deliberately not added: it would make N
worktrees of one repo serialize fetches that run in parallel today.
* fix(native-chat): keep the attachments on a Claude turn that pasted images
A Claude turn carrying pasted images reached native chat with no images at all —
no thumbnails on mobile, and not even an attachment chip on desktop. Nothing
showed that the message had any.
Both carriers were being dropped:
- Claude records the paths in a companion turn marked `isMeta`, holding one
`[Image: source: <path>]` text block per image. The decoder treats an `isMeta`
user row as injected, filters it down to tool-result blocks, and returns null
when none remain — so the whole row went away.
- The prompt row's own `image` blocks are `{source: {type: 'base64'}}`, which
carry no url or path, so `imageRefBlock` drops them too.
With the companion gone, `isImageSourceUserTurn` could never fire and the fold in
`normalizeImageTranscriptMessages` was unreachable on the Claude path.
Surveying every transcript under `~/.claude/projects`: 238 of 241 image-source
rows are `isMeta`, across every versioned release (2.1.220 through 2.1.237); the
3 that are not carry no version field at all. 38 of those rows hold more than one
content block, which also defeated the single-block rule in
`isImageSourceUserTurn`.
Let image-source text survive the injected-turn filter, and recognize a turn
whose blocks are *all* markers rather than only a lone one. An ordinary injected
turn (a skill preamble, a compact summary) is still dropped, and a turn that
mixes prose with a marker is still not an image-source turn.
Carrying the paths keeps the payload small; decoding the base64 instead would put
hundreds of KB per image on the wire to mobile.
* fix(native-chat): preserve image companion ordering
* fix(native-chat): keep image companions turn-local
---------
Co-authored-by: Merge Sim <sim@local>
* fix(native-chat): stop rendering tool output as the agent's streaming reply
A tool result could appear in native chat as a raw, un-collapsed "assistant"
bubble that never went away for the rest of the turn — on mobile it showed up
as a wall of a source file's contents, prefixed by "Exit code 1".
Providers publish a tool's stdout/error as `lastAssistantMessage` so status
cards and dashboard rows can preview what the agent just did. Native chat reuses
that same field as its live streaming bubble, so the preview rendered as prose.
For Claude the preview is *only ever* tool output mid-turn: claude-tool-fields
writes real prose exclusively at Stop, so the bubble could never contain an
actual streaming reply.
It also could not be retired. The bubble hides once a transcript assistant block
leads with the streamed text, and tool output never lands in one — so the only
remaining exit was the turn ending, which is why a long tool-heavy turn pinned it
on screen.
Carry provenance instead of changing what the status surfaces show: mark the
writes that come from a tool result/error, keep the flag in lockstep with the
value it describes through the listener merge, and have both native-chat
streaming paths ignore a flagged preview. Status cards, dashboard rows and
automation capture are untouched.
The wire field is optional, so an older host that never sends it keeps today's
behavior rather than silently suppressing previews.
* fix(native-chat): preserve tool output provenance through renderer sync
* fix(native-chat): retain preview provenance in Claude roster state
* test(native-chat): cover restored tool preview provenance
---------
Co-authored-by: Merge Sim <sim@local>
Two small changes to the Git metadata read path. Neither has a user-visible
effect on any platform except for a malformed `.git` gitfile, described below.
1. resolveGitMetadataPath's third parameter becomes an options object
`{ platform?, wslDistro? }`. A caller that knows which distro wrote a pointer
can now say so, where previously only a WSL UNC base path could. The distro
encoded in the base path still outranks the caller's, and translation only
happens when the reading host is win32, so a caller-named distro cannot make
a POSIX host fabricate a Windows path. The UNC-base branch is exempt from
that gate because that spelling only exists on Windows. Main's other
contracts are verbatim: never null for a non-empty pointer, and a drvfs
pointer keeps its drive spelling even when a distro is named. Both production
call sites (repo-git-marker-scan.ts) pass no options, so they are unchanged.
2. The `.git` gitfile marker parse moves into one shared function,
parseGitdirMarkerPayload: `gitdir:` at the start of the file, payload
trimmed, empty payload rejected — git's own read_gitfile_gently rule.
resolve-git-dir.ts and repo-git-marker-scan.ts both call it; the latter had a
near-identical private copy and is behaviorally identical after the swap
(verified across twelve marker spellings; the only divergence, a
whitespace-only payload, already resolved to null one call further down).
Main's `/^gitdir:\s*(.+)\s*$/m` in resolve-git-dir captured trailing padding
into the path and honored a `gitdir:` line anywhere in the file.
Per-platform delta: none on macOS, Linux, native Windows, WSL, SSH, relay, or
folder workspaces. The wslDistro option is inert; this change adds no caller.
For a malformed `.git` gitfile, padding is now stripped (strict improvement), a
whitespace-only payload falls back to `<worktree>/.git`, and a `gitdir:` line
that is not the first line is no longer honored — a narrowing, since main could
return a working gitdir there. All four resolveGitDir consumers already degrade
through a catch, so that case reports no sparse state / conflict operation /
diff stamp rather than failing.
Six other hand-rolled `gitdir:` parsers remain, including the relay's SSH copy;
converging them is its own change.
When Orca's runtime is a WSL distro but the repo sits on a Windows drive, git
inside the distro writes `/mnt/c/...` into a worktree's `.git` gitfile and its
`commondir`, while Orca reads those files back through Win32.
`repo-git-marker-scan` returned the pointer verbatim, Windows read it as
drive-relative `C:\mnt\c\...`, and the worktree was reported `invalid`.
Move that resolver out of `repo-git-marker-scan` into
`src/shared/git-metadata-path.ts` and give it exactly one new case: on win32, a
drvfs pointer resolved against a base path that is not a WSL UNC path now gets
its drive spelling. Every other base/pointer/platform combination is
byte-identical to the deleted helper, verified differentially across a
base x pointer x platform matrix — macOS, Linux and native Windows are unchanged.
`toWindowsWslDrivePath` is factored out of `toWindowsWslPath` so the drvfs
matcher has one home; `toWindowsWslPath` itself is unchanged for all inputs,
including the line terminators JS `.` excludes (fuzzed 2M inputs, 0 divergences).
This changes the marker scan's verdict only. `resolve-git-dir.ts` and the relay's
own copy still `path.resolve` the same `/mnt/c/...` pointer in the Win32
namespace, so a worktree that is now accepted still degrades quietly in conflict
detection, sparse-checkout detection, the diff stamp and worktree listing. Those
parsers are deliberately untouched here; see the PR description.
Co-authored-by: Neil <neil@example.com>
* refactor(runtime): split OrcaRuntimeService into focused modules
* test(runtime): cover admission tiers and strict worktree reconciliation
* fix(runtime): preserve owner and structured session visibility
* fix(runtime): port post-extraction compatibility fixes
* fix(runtime): preserve skill-share cancellation barrier
* test(runtime): update identity inventory after extraction
* fix(runtime): preserve hook transport environment cleanup
* fix(runtime): consolidate idle probe imports
* test(runtime): retire split file process allowlist entry
* fix(runtime): route child process types through shared boundary
* test(runtime): preserve worktree host metadata precedence
* fix(runtime): update extracted test seams
* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract
Audit follow-ups for the OrcaRuntimeService split:
- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
type checking. The split's linear mixin chain cannot express forward
references yet, so the existing suppressions are grandfathered; the baseline
may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
after the first statement, where TypeScript ignores it, so the module was
already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
argument. The split widened it to optional and patched the resulting error
with `stopConfirmed === true`; an omitted argument would have silently taken
the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
so one left out of the list would silently stop running.
* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped
Audit findings against the refactor's true base (ad5ba2572e):
- retirePtyAgentLaunchAuthority collected pane keys after deleting the
restored-authority receipt instead of before it. collectPaneKeysForPty reads
that receipt, so a receipt-only pane lost its key and never had its agent-hook
compatibility authority retired. on-pty-exit.ts already carried a comment
naming this exact invariant.
- The PTY-exit path kept orchestrationMailboxNotifications.retirePty but lost
the loop that schedules a debounced mail-pointer repoint for the dead pty's
terminal handle and any run bound to its panes. Restores the schedule call
count to 7, matching base.
- subscribeToPtyExit lost isPtyKnownExited's leaf fallback and its
post-registration lifecycle-generation recheck. leavesByPtyId is rebuilt from
the renderer graph independently of ptysById, so a leaf can outlive its pty
record; without the fallback a caller waiting on an already-dead pty never
gets released.
- The chain root declared `[key: string]: unknown`, which base had nowhere. It
leaked through the exported runtime type into every consumer, so any misspelled
member access typechecked as unknown instead of erroring, and it accounted for
957 of the suppressed errors. Removing it costs zero type errors.
* fix(runtime): restore escalation prose and unscoped automation publication
Two more behaviors the split dropped, each with a regression test that fails
against the pre-fix code:
- The worker-exit escalation stopped deriving its title through
buildOrchestrationTaskDisplayMetadata and inlined `task.spec` instead. That
ignored an explicit task_title, dropped the single-line normalization and the
80-character bound, and turned the no-spec case into a quoted, duplicated id.
A multi-paragraph spec landed verbatim in the coordinator's banner. The
existing 11 tests all use short single-line specs, where the derived title and
the raw spec are identical, so none of them could see it.
Also reverts an added `if (!handle) return` guard: the dispatch lookup is
deliberately keyed on the pane as well, because a reminted handle no longer
matches the row while the pane identity outlives the remint.
- updateAutomation stopped going through automationChangePublications and
published `source` unconditionally while gating the fallback on a non-null
destination. A destination the store can no longer name then published only
the stale source, so subscribers scoped elsewhere kept rendering a row that
had left them — the exact case the helper documents. The helper had been left
with zero callers; all three sites use it again.
* fix(skills): stop swallowing lookup errors and hard-erroring on non-ssh hosts
Follow-ups from auditing the skill install path against the refactor's base:
- resolveWorktree wrapped showManagedWorktree in `.catch(() => null)`, so a
transient git or IO failure surfaced to the user as
skill-install-workspace-not-found with the real cause discarded. Errors
propagate again; a genuine id mismatch still returns null.
- resolveSkillSshTarget threw skill-install-workspace-host-unavailable when the
execution host was neither local nor ssh, on both the repo and folder
branches. Base gated these on connectionId, so a runtime-owned repo simply
was not an SSH install and fell through to the local path. Both return null
again, and the error code the split invented is now unreferenced.
- listManagedSkillInstalls awaited the receipt walk and the worktree resolve in
sequence. They are independent and either can hit disk, WSL, or an SSH scan,
so Promise.all is restored.
Deliberately unchanged: resolving the worktree through listResolvedWorktrees
rather than showManagedWorktree, which disambiguates a worktree id colliding
across hosts and is covered by its own test, and the SSH-folder
skill-install-ssh-dispatch-required throw, which matches the repo branch.
* fix(runtime): merge duplicate worktree-logic imports
The #17448 port added a third import from ../ipc/worktree-logic, which the
code-quality oxlint config rejects under --deny-warnings. Plain oxlint does not
flag it, so it only surfaced in CI's static analysis job.
* ci: run the ts-nocheck ratchet in PR checks
pr-workflow-lint-parity requires every leaf command in `pnpm lint` to have a
matching step in pr.yml. The ratchet was wired into lint but not the workflow,
so PR CI would not have enforced it.
* Merge remote-tracking branch 'origin/main' and retry the paired-host launch evaluate
main advanced 9 commits; none touch the orca-runtime.ts this branch splits, so
nothing needed porting.
CI failed twice on `Execution context was destroyed` thrown from
headless-paired-runtime-host's first `evaluate` after launch — a different spec
each run, which is the signature of the flake #17780 describes rather than a
regression. That commit added retryTransientMainEvaluate and adopted it in five
helpers but not this call site, even though its docblock names exactly this
case: the first evaluate after electron.launch() resolves, before the app is
ready. Wrapped it the same way.
* Avoid Linear read re-fetches when workspace scope is unchanged
Derive a stable scope signature that captures only the connected state
and workspace identity, ignoring volatile metadata like displayName.
Use this in dependency tracking so Linear searches don't re-run on
status updates that don't affect which issues can be queried.
* Expand workspace scope to detect credential and org changes
Cache invalidation key now includes credentialRevision and organizationUrlKey for
both workspace and viewer, ensuring Linear reads re-fetch when credentials rotate or
organizations are renamed — fields that affect what read operations return.
* Include activeWorkspaceId in workspace scope signature
URL lookup falls back to the active workspace even when all workspaces
are selected, so activeWorkspaceId must be part of the scope signature
to ensure reads are keyed correctly.
Restart-survival polls treated a recycled renderer as a hard failure.
Wrap those evaluates so "Execution context was destroyed" is a pending
miss. Windows package-lane teardowns after a force-kill used rmSync
with force:true only, which does not absorb EPERM; put them on the
shared maxRetries:8 policy.
* perf(relay): cache process-table descendant indexes
* fix(relay): keep the process-table index first-wins and narrow
Two defects in the memoized index this PR introduced.
- Restore the first-wins duplicate-pid tie-break the relay had as
`rows.find()`. A process whose argv contains a newline makes `ps` print a
continuation line that the lenient parser can accept as a spurious row
duplicating a real pid; that row always FOLLOWS the real one, so last-wins let
it capture the pane's foreground. The rule now lives in
`buildProcessTableIndex`, so the batched evidence resolver's `byPid.get(rootPid)`
root lookup gets the same semantics the subsystem had before indexing.
- Build only the two indexes a resolver reads. `byPgid`/`byTpgid` have no readers
repo-wide, and delegating to a four-map build made a one-pane relay pay more
per 500ms capture than the single `childrenByParent` map it replaced --
a regression in the majority topology, in a PR whose point is relay CPU.
Matches the same deletion in #17763 line for line so whichever merges second
resolves trivially.
* fix(diff): close large-diff deferral review findings from #17521
Deferral keyed "no line counts" off the untracked area, which both prompted
ordinary untracked binaries and silently auto-loaded every tracked row when a
status pass skipped counting (entry cap hit, numstat failed) — the freeze case
the deferral exists for. Decide from the path instead: rows that render as a
preview or a binary stub stay automatic, everything Monaco would open as text
defers.
Also give all three combined-diff virtualizers one shared row estimate, so the
PR-review viewers stop estimating a deferred/in-flight large row at 88px while
DiffSectionItem renders it at 188px, and drop the dead isLoadOnDemand
parameter that estimate covered.
* fix(diff): stop deferring cheap uncounted rows the extension list misses
The path-only rule relocated friction rather than removing it: every uncounted
row deferred unless its extension was in BINARY_FILE_EXTENSIONS, so two classes
of tracked row flipped to a "Large diffs are not rendered by default" prompt
they had never shown. Tracked binaries outside the list (this repo's own
resources/build/icon.icns, plus .tiff/.avif/.psd/.parquet and every
extensionless binary) get '-\t-' from `git diff --numstat`, and a submodule
whose only change is untracked content inside it gets no numstat row at all
while porcelain v2 still reports `1 .M S..U ... sub`. Both are cheap, and both
are unreachable from a hardcoded extension list — verified against real git.
OR the extension check with two signals already on the entry. A submodule row
diffs to a "Subproject commit" line or two whatever it contains, so it is
always cheap. And an uncounted row whose siblings in the same pass DID get
counts is uncounted for a reason of its own: for a tracked row that reason can
only be numstat's binary marker. Untracked rows keep deferring either way,
since the scan also skips them past MAX_UNTRACKED_LINE_COUNT_BYTES and their
size is exactly what is unknown. No new field crosses git status, the wire, or
the section cache; `submodule` and the sibling counts are already there.
Fan-out, accepted deliberately: when a pass counts nothing at all — didHitLimit
at DEFAULT_GIT_STATUS_LIMIT, or runNumstat returning null — no row has a
counted sibling, so the whole combined diff renders as Load prompts. Keeping
it. Over 1000 changed entries is precisely the freeze this deferral exists for,
and auto-loading that many unbounded Monaco models is the bug, not the
mitigation; a numstat failure leaves every size genuinely unknown. Each row
still has its own Load diff button, so nothing is unreachable — the only thing
missing is a bulk "load all", which would reinstate the freeze on demand.
* fix(diff): scope the counted-siblings signal to one counting pass
hasCountedSiblings was one boolean over the whole entries array, but that array
is not one counting pass. combined-all — the default whenever a branch compare
exists — concatenates uncommitted rows with branch-compare rows, and even within
the uncommitted set staged and unstaged are separate numstat calls that fail
separately. So a single counted branch row vouched for an uncommitted pass that
counted nothing (numstat null, or didHitLimit at DEFAULT_GIT_STATUS_LIMIT), and
every uncounted row in it auto-loaded into exactly the Monaco freeze the
deferral exists to prevent: the guard was off in the default view.
Collect the passes that actually counted something, keyed by staging area for
status rows and 'compare' for branch/commit rows, and ask that set per row.
Untracked rows are unaffected — they never consult the signal.
Class 1 of the charter (tracked binaries outside BINARY_FILE_EXTENSIONS) stays
open, deliberately. Porcelain v2 reports a modified binary as `1 .M N... 100644`
— indistinguishable from text — so only `git diff --numstat`'s `-\t-` knows, and
that stdout is parsed on the host (shared/git-uncommitted-line-stats.ts) for
both the local and relay status paths. The renderer sees entries, not numstat,
so surfacing it per row means a new field on GitStatusEntry and
GitBranchChangeEntry that also has to be re-applied in two attachLineStats
copies and in the line-stats reuse cache, which persists only {added, removed}
and would silently drop it. The one existing field that could carry it —
added/removed set to 0 — changes what the host publishes to old clients and
mobile, contradicts the documented "undefined for binary files" contract, and
collapses the undefined-vs-zero distinction the virtualizer's height estimate
reads. So a lone tracked .icns still shows the load prompt; not worth a wire
field, and not worth another hardcoded extension.
* fix(diff): stop calling an uncounted diff large in the load prompt
The deferral prompt had one sentence for two different reasons. A row over
MAX_AUTOMATIC_DIFF_CHANGED_LINES really is large. A row with no counts at all —
numstat's binary marker, a pass that skipped counting — is deferred because its
size is unknown, and "Large diffs are not rendered by default." is simply false
for it: a lone tracked resources/build/icon.icns with no counted sibling in its
own pass is 4 KB and still says large.
Split the copy on the counts the section already carries. No new field on the
entry, nothing across the wire, no change to attachLineStats or the line-stats
cache — the predicate is renderer-local and mirrors the uncounted branch of
shouldLoadCombinedDiffOnDemand, so the two stay in step.
Follow-up defect fixes for the batched PTY-inventory evidence path (#17525),
now on main.
- One memoized `ps` capture serves both the lenient and strict views. The two
readers ran byte-identical argv behind separate caches, so a relay serving
both forked `ps` twice per 500ms window — the doubling issue #6288 removed.
- Drop the `byPgid`/`byTpgid` indexes no resolver reads, plus the zero-caller
`parseProcessTableRowsStrict` and `getFreshStrictProcessTableSnapshot`; the
batch resolver now reuses the shared index lookup and candidate score instead
of private copies.
- Restore `getForegroundProcessName`'s ladder contract: the extracted table scan
answers null again, so an unconfirmed wrapper fallback publishes the
recognized (normalized) name rather than node-pty's raw one.
- Pin the SHIPPED `pty.listProcesses` path: one capture and one linear row pass
for N panes, and node-pty's own name (never "shell") when the capture cannot
disambiguate a `node`/`python` wrapper.
- Pin the hidden-pane cadence gate in the production option shape, and move the
strict-parser coverage next to the parser it tests.
* fix(remote): distinguish transport from runtime availability
* fix(remote): preserve transport diagnostics for unavailable runtime
* fix(remote): propagate transport diagnostics to host setups
* fix(remote): keep unavailable runtimes out of ready setups
* fix(remote): preserve unavailable runtime state in settings
* fix(remote): preserve reconnecting runtime state
* fix(remote): guard stale settings connectivity
* fix(remote): preserve diagnostics after main merge
* fix(i18n): preserve translations during runtime status merge
* fix(remote): refresh settings row health from store
* fix(remote): refresh settings row health from store
* fix(remote): clear diagnostics generations in tests
* fix(settings): refresh runtime availability summary
* refactor(runtime): split status slice types
* refactor(runtime): reuse status app state type
---------
Co-authored-by: Merge Sim <sim@local>
* Show live tool progress in native chat
* fix(native-chat): scope live tool indicator to current turn
* fix(native-chat): settle orphaned live tool rows
* fix(native-chat): keep live tools running without lifecycle metadata
* fix(native-chat): keep working status stable during streaming
* fix(native-chat): anchor turn status below prompts
* fix(native-chat): preserve turn status and legacy tool activity
* fix(native-chat): limit turn status UI to structured Codex
---------
Co-authored-by: Merge Sim <sim@local>
* perf(git): bound ref and worktree scans
* fix(repo-search): clamp oversized ref limits
* fix(worktree): keep strict worktree listing unshared
The shared-scan re-export flipped every `listWorktreesStrict` caller from an
isolated subprocess to the coalesced scan. `git worktree prune` in the removal
recovery path does not bump the scan generation, so a post-prune verification
could join a pre-prune scan, see the stale row, and report a successful removal
as a stale registration. The same gap defeats the post-archive-hook rechecks
that exist to catch an external Git client locking the row.
Restore the unshared export and make coalescing opt-in via
`listWorktreesSharedStrict`, which existing callers already use deliberately.
* fix(git): separate a proven absent ref from a failed probe
`show-ref --verify --quiet` exits 1 for a missing ref, but so does `wsl.exe`
when its own launch fails, so reading any exit 1 as absence collapsed
`unverifiable` into `exited`. A genuine miss prints nothing while a wrapper
failure always explains itself, so require empty stderr alongside the exit
code; a runner that reports no stderr at all keeps its exit-code contract.
That same signal removes a spawn regression: `show-ref` is a direct-git read
under WSL, and the runner retried any numeric exit through the user's
interactive login shell. The replaced `for-each-ref` exited 0 on a miss, so
absence never retried; every absent probe now would. Treat a quiet exit 1 as
Git control flow and skip the fallback.
Also narrow the hosted-review suffix fallback: the replaced
`refs/remotes/*/<base>` could not cross a slash, but `show-ref -- <base>`
matches at any depth, so `origin/feature/main` answered a query for `main`
and submitted a review against a base the provider rejects.
Refresh the real-binary compatibility contract to the shipped excludes, and
assert exact probe concurrency rather than an upper bound so a regression to
serial probing fails.
* fix(worktrees): preserve user workspace names across branch changes
* test(worktrees): cover pinned rename metadata
* fix(workspaces): address display-name review edge cases
* fix(workspaces): keep automatic names fresh across refreshes
* fix(workspaces): preserve legacy CLI labels
* fix(workspaces): preserve display-name provenance across hosts
* fix(workspaces): honor legacy display-name provenance
* fix(workspaces): fence display-name refresh races
* fix(workspaces): accept peer renames from provenance-less hosts
The old-host preserve fence kept a pinned local label on every refresh,
which also suppressed a legitimate rename another client persisted
through the same host until app restart. Narrow it to labels the host
re-derived itself (branch short name, or path basename when detached);
any other changed label in a mode-less response is explicit meta a peer
wrote there. Stale prior-label responses stay covered by the downstream
staleness fence, in-flight writes by the pending fence.
* refactor(workspaces): unify display-name pin derivation
Three call sites (renderer optimistic update, local IPC updateMeta
handler, remote worktree.set handler) each restated the same formula;
a future edit to one would silently skew provenance between paths.
* feat(ssh): batch process evidence in PTY inventory
* fix(ssh): accept Linux kernel process rows and make no-evidence polling push-driven
* fix(ssh): preserve process evidence polling semantics
---------
Co-authored-by: Merge Sim <sim@local>
* fix(codex): safely re-land WSL direct homes
* fix(codex): finish WSL direct-home cutover
* fix(codex): coalesce WSL launch hook installs
* perf(codex): avoid duplicate retired WSL session scan
* fix(codex): retain canonical WSL retired-home path
* fix(codex): fail closed before retiring WSL auth
* fix(codex): reopen WSL drain after rollback
* fix(codex): preserve WSL source on unknown panes
* fix(codex): harden repeated WSL runtime drains
* perf(codex): bound pending WSL session scans
* fix(codex): recover invalid WSL session watermarks
* fix(codex): validate retained WSL scan state
* fix(codex): accept durable WSL scan state
* test(codex): cover the drain's inode-identity guard against destination replacement
Removing the four `target_auth -ef temporary_destination_auth` assertions left
all 33 apply-script tests passing, so a regression deleting them would have
shipped silently. Reproduced before writing this.
A hash check cannot catch the case. The pinned hard link keeps the original
inode, so it still hashes correctly after another writer atomically renames a
different file over the destination path; only inode identity sees it. Without
the guard the script exits 0 and retires the source, leaving the user holding
bytes nothing validated. The new case asserts the source survives.
The harness is split by responsibility so no file exceeds its max-lines budget:
fixtures, the coreutils interference shims, the run types, the apply runner, and
the recovery/absent runners. The atomic-rename hook is deliberately separate
from the in-place rewrite shim because different guards catch them.
* fix(codex): keep the split drain harness inside the child-process boundaries
Extracting the harness into non-test modules moved it out of the exemptions the
single test file had: three new files import child_process, and two spawned
without windowsHide.
Adds the three to the import allowlist, and sets windowsHide on the spawns
rather than exempting them - the flag is correct for these calls regardless of
the ratchet, and they are skipped on win32 anyway.
---------
Co-authored-by: Merge Sim <sim@local>
* fix(ssh): fence stale kills and retired pane replay
* fix(ssh): support cancellable interactive authentication
* fix(ssh): await remote catalog before snapshot adoption
* fix(pty): contain Windows ConPTY input failures
* fix(power): avoid redundant macOS display blocking
* perf(editor): narrow markdown override subscriptions
* fix(quick-open): close directory handles after reads
* refactor(linux): remove unused proc socket scanner
* fix(usage): apply flat Sonnet 4.6 pricing
* ci: prime Node next native test cache
* docs(skills): resolve snapshot cleanup data path
* fix(ssh): recover install locks after host reboot
* test(ssh): recognize boot-aware install locks
* test(ssh): prove previous-boot lock recovery live
* test(wire): pin pre-metadata release coverage
* fix(terminal): preserve remote tab ownership through recovery races
* test(runtime): fence replaced terminal handles in agent guard
* fix(ssh): preserve remote snapshot authority across polls
* fix(pty): contain late ConPTY output EPIPE
* test(pty): register Windows exit watcher before kill
* fix: close SSH and tab readiness race gaps
* fix(tabs): retain headless order and placeholder titles
* fix(build): avoid parallel electron-vite config race
* test(windows): avoid MSYS temp path rewriting
* test(windows): avoid killing exited PTY
* fix(pty): avoid late ConPTY input teardown race
* fix(terminal): sync reconnect error ownership after commit
* fix(runtime): use canonical worktree identity comparison
* test(ssh): assert complete cold-hydration baseline
* test(windows): invoke quoted retention fixture via PowerShell
* test(windows): read ConPTY grid through mode con
* fix(terminal): publish PTY replacements atomically
* fix(terminal): infer stale identity on reattach
* fix(terminal): fence stale pane PTY callbacks
* fix(terminal): fence stale pane binds after rebind
* fix(terminal): reject stale pane transport callbacks
* fix(terminal): fence mirrored reattach spawn callbacks
* fix(terminal): replace stale pane PTYs on remount
* fix(ci): size the Windows launcher-compile test budget from measurement
`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.
The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.
Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.
This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.
The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.
* fix(terminal): fence stale remount reattach ownership
* fix(terminal): reconcile mounted pane identity after replacement
* fix(terminal): fence stale reattach fallback ownership
* fix(terminal): fence deferred SSH reattach ownership
* fix(terminal): fence stale split pane ownership callbacks
* fix(terminal): keep stale spawns from consuming startup
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(worktree): gate agent activation on the live surface census, not renderer state (STA-5701)
* fix(worktree): seed a pane when the surface census cannot prove ownership (STA-5701)
Failing closed must not also fail silent. When the census is unverifiable
the sweep adopts nothing and mints nothing, yet the gate still reported
'adopted' — and both callers suppress their own seeding on any outcome but
'empty', so the workspace ended with zero surfaces. The sweep now reports
whether any live PTY holds a surface and the gate hands the caller its seed
when none does. Also folds equivalent workspace-path spellings in the census
index and in exact-surface binding, so a host row spelled differently is
neither dropped (mint a duplicate) nor unbindable (no pane).
* fix(worktree): name the live PTYs the surface census declined (STA-5701)
The adoption sweep can leave a live PTY without a surface — an unreadable
census, two host surfaces claiming one PTY, or a host-named leaf the
persisted layout does not have. The gate already stops reporting 'adopted'
in that case so the caller seeds a shell, but the decline itself was mute.
- adoptLiveWorkspacePtySurfaces now returns { surfaced, declinedPtyIds }
and the gate warns with the workspace and the PTY ids left unsurfaced.
- Pin the host-named-leaf decline, which had no test either way.
- Pin the superseded-inventory race in terminal.list: a concurrent refresh
makes hostScope.hostIds empty, which is what makes the renderer's
'unverifiable' verdict reachable on a plain local machine.
* perf(wsl): warn at project-add when the tree sits on a Windows drive
Worktree placement now puts new workspaces inside the distro, but a project
whose own tree is on C:\ still pays the 9p/drvfs crossing on every git command
it runs — measured at ~20x for a clean `git status` against the same tree on
ext4. Nothing in the UI says so, so the project just feels slow.
Warn once, right after the add succeeds, naming the distro the project's git
actually runs in. The advisory is wrapped so it can never fail the add.
Two path shapes cross the boundary and both warn: a Windows drive path under a
WSL project runtime, and the UNC spelling of a distro's own drvfs mount
(\\wsl.localhost\Ubuntu\mnt\c\...), which crosses it however the runtime is set.
A tree already inside the distro, a drive path under Windows-host git, a plain
UNC share, and every POSIX/SSH path stay silent.
* chore(i18n): register the WSL filesystem boundary advisory keys in en.json
* refactor(agent-hooks): drop the unused per-agent hook status IPC surface
No renderer, CLI, or mobile caller invoked window.api.agentHooks.*Status; main
already reads install status through MANAGED_AGENT_HOOK_STATUS_READERS. The
14 handlers had also drifted (kimiStatus existed in main/preload but not in
AgentHooksApi or the web stub).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(tui-agent-config): default launchCmd and expectedProcess to detectCmd
32 of 36 entries repeated the binary name three times. Entries are now
authored in a source form where both default to detectCmd and resolved once
at module load, so TUI_AGENT_CONFIG keeps its exact shape for consumers
(verified equal to the previous table).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(mobile): derive the agent order, labels, and picker from src/shared
The mobile mirror (and its regex-over-desktop-source parity test) predates
mobile importing runtime values from src/shared, which it now does in a dozen
modules. Only the favicon-domain map stays mobile-local because desktop's lives
in the renderer catalog next to bundled ?url imports. The parity test now
imports the real registries and also checks label parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(web): align preload surface after hook IPC removal
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
- Jira rejects a bare string for reporter/user-picker fields on issue
create, so shape customFields values into {accountId}/{name} objects
for keys the caller flags via userFieldKeys.
- Seed required user fields with the authenticated viewer by default
and add a searchable user picker (jira.searchUsers) so users aren't
forced into free text for reporter/custom user fields.
* fix(agents): keep OMP identity and forward ask/approval events (STA-4130)
A live OMP pane was re-owned as Pi because the generic pi-compatible
fallback always won, ask events blocked without a question payload, and
OMP suppressed tool_approval_* unless an extension registered handlers.
Mark Pi as the title-group fallback so a specific OMP identity is not
downgraded, publish OMP ask input as the existing questions envelope, and
forward tool_approval_requested/resolved onto blocked/working.
STA-4130
Related to #14278
Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
* fix(agents): keep launch Pi ownership over OMP wrapper frames (STA-4130)
The pi-compatible fallback treated every generic Pi owner as inferred, so an
explicit launch-Pi pane (and a launchless Pi pane with an OMP-shaped title)
was re-owned as OMP. Launch provenance now stays authoritative; only an
inferred status-frame owner yields to a specific sibling, and same-group
titles no longer count as reuse.
STA-4130
* fix(agents): drop Pi wrapper idle titles while OMP hook is active (STA-4130)
Title-completion suppression compared pick-a-winner ownership, so a Pi ready
frame looked like a different agent than a live OMP hook and fired a spurious
task-complete notification. Reuse checks now use the title-identity group.
STA-4130
* fix(agents): restore OMP approval forwarding after merge
* fix(agents): restore title-owner API after merge
* test(agents): update identity inventory ratchet
---------
Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
Co-authored-by: Merge Sim <sim@local>
* fix(omp): read Pi/OMP static state-title markers and retire stale spinners
OMP 17.2.12 replaced its animated braille title frames with static markers
on WSL/ConPTY (`π : working`, `π > idle`, `π ! needs input`). Orca read all
three as idle, so a working OMP pane lost its status, and a synthetic title
spinner started by an earlier hook kept rotating after its status row was
gone.
Classify the markers from one shared table so a later upstream punctuation
change is a row, not a reparse, and stop the spinner when the hook row it
stands in for is cleared or dismissed.
Fixes#13890
* test(omp): preserve static state titles during normalization
---------
Co-authored-by: Merge Sim <sim@local>
* Fix orchestration CLI recovery, settled-Dispatch mail, and guide defects
Five reported orchestration CLI defects, verified individually before fixing.
Two were real code defects, one was a docs error, one was correct as-is, and
one was correct on both ends except for its recovery wording.
- Mail addressed to a settled `dispatch:<id>` was accepted and silently dropped.
Local sends bypassed the settlement check the federated branch already had, so
the caller was told success for a delivery no worker would ever read. Reject
with `dispatch_inactive` and name the Run mailbox to use instead.
- A lost mutation response offered no read-only way to ask whether it took
effect. `--retry-request` does dedupe correctly, but the recovery guidance
emitted a query command only when the payload carried a dispatch id, which is
exactly what a lost response lacks. Add read-only
`orca orchestration request-show --request <id>` over the durable receipt
ledger, and always emit a read-only step before the keyed retry.
- The bundled `orca-cli` guide documented `check --unread --inject`, a flag the
parser rejects. Correct it to `--format` and add a ratchet that runs every
orchestration invocation in the bundled guides through the real CLI parser.
- `check --json` is one stdout document and its keepalives are stderr-only; the
reported `Extra data: line 2` came from merging the streams. Document the
contract rather than changing the wire.
- A rejected lifecycle message is loud on both ends already, but the rejection
never named the flag that supplies the missing capability. Name it.
* Harden orchestration mutation recovery guidance