* fix(terminal): defer metric option writes to unmeasurable panes
Writing fontSize/fontFamily/fontWeight/lineHeight makes xterm re-measure
cell size against the pane's current box. A hidden or mid-layout pane can
measure a wrong-but-nonzero size, which latches (hasValidSize) and mis-keys
the shared WebGL glyph atlas until a manual resize — the stuck variant of
the P0 bold/blurry-font reports.
Metric writes now land only on measurable panes; otherwise the latest
values park per-pane and flush on the next safe fit or reveal (with a refit
on the light tab-resume path, which otherwise skips fitting). Measurability
helpers move to pane-fit-measurability.ts to stay under the pane-fit.ts
line cap.
* fix(terminal): key metric deferral by terminal, not pane view
getPanes() returns a fresh toPublicPane() wrapper per call, so a
WeakMap keyed on ManagedPane never matched across call sites: deferred
metric options were dropped, not deferred. Key on pane.terminal, which
is carried by reference and dies with the pane.
Also from review:
- flushDeferredPaneMetricOptionsIfMeasurable checks the pending WeakMap
before the measurability probe, so the common no-deferral case costs
zero forced style/layout on every reveal.
- applyTerminalAppearance skips the apply (and the probe) when all five
values are already live and nothing is parked; any settings write
re-runs the pass over every mounted pane, and arming a no-op deferral
would trigger a refit on the next reveal.
- fitRevealedPane flushes first: its pixel/grid checks can both no-op
and return without fitting, stranding parked options.
- Font zoom folds its direct fontSize write into any pending deferral so
the flush inside safeFit cannot clobber the user's zoom.
Corrects comments that asserted a cell-size re-measure mechanism xterm
does not have: CharSizeService measures via OffscreenCanvas TextMetrics,
independent of the pane box, and only fontSize/fontFamily re-measure.
Test fixtures now allocate a fresh pane view per getPanes() call, which
is what production does and what hid the keying bug.
* fix(terminal): re-check the fit floor after a metric flush
performSafeFit evaluated the min cols/rows gate with the pre-flush cell
size, then flushed and fit unconditionally. A large font jump on a
narrow pane passes the gate at the old size and lands under it at the
new one, so fit() pinned the PTY to the tiny grid the floor exists to
reject. Re-check after a flush that actually landed.
The parked values still apply, so the pane is never stuck on stale
metrics; only the fit is skipped.
* fix(terminal): route a reveal metric flush through the stable fit
fitRevealedPane's new flush branch called safeFit directly, which is
exactly what the function's contract forbids on reveal: resumeRendering
has just re-attached WebGL, whose cell metrics transiently differ from
the DOM renderer's, so a raw fit can propose a one-column-off grid and
reflow — and xterm's wrap/unwrap is not a perfect inverse, leaving a
diff-painting inline TUI corrupted.
A landed flush leaves pixels unchanged with a diverged grid, the same
shape as a snapshot resize, so it takes the same steady-grid repair.
A real resize still fits synchronously, after the flush.
Reachable via window wake, which calls fitAllRevealedPanes with no
pre-flush loop.
* fix(terminal): gate metric writes on the pixel box, not the fit floor
canApplyPaneMetricOptions reused canMeasurePaneForFit, whose >=8 cols /
>=4 rows floor exists to stop a fit pinning the PTY to a sliver. But the
divider clamp is 50px, which clears the 48px pixel floor and proposes
~5 cols — so a pane dragged to the clamp deferred every font change and
never flushed: it never hides, and its box never changes, so no reveal
and no ResizeObserver entry ever arrives. It rendered a stale font until
widened, where pre-PR the write was unconditional.
Gate metric writes on display plus the pixel box only. Hidden panes and
the transient worktree-switch overlay are near-zero, so they still
defer — the deferral's purpose is unchanged. The cols/rows floor stays
on the fit, including the post-flush re-check in performSafeFit.
Apply and flush share the same predicate, so no "applies but never
flushes" state can open up.
* fix(terminal): flush heavy reveal metrics after WebGL resume
* fix(serve): recognize CLI-form serve args on the Electron process
When the binary is launched as `… serve --port …` without the CLI rewrite
that injects `--serve`, normalize argv so isServeMode, headless GPU flags,
and serve option parsing all engage.
Preserves existing `--serve*` flag behavior for the CLI-spawned path.
Fixes#12677
* fix(serve): treat only CLI subcommand position as serve
Parse bare `serve` as the first positional token after flags/values so an
option value named `serve` cannot enable headless mode.
Addresses CodeRabbit on #12818.
* fix(serve): keep CLI redirects ahead of the serve argv rewrite
Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve`
positional with `--serve`, so the redirect's command-name lookup saw a port
number and bailed — dropping AppImage serve launches out of the CLI path.
Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads
the next token) and the mixed `--serve --port` form, so a security-shaped flag
like `--no-pairing` can no longer read as accepted while pairing stays on.
Map lookups replace `in` on object literals, which turned a stray `serve
toString` positional into a function spliced onto argv.
* fix(serve): close the CLI-form serve gaps found in review
second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`,
so a duplicate `<binary> serve --port …` — the ExecStart shape documented in
docs/reference/headless-linux-server.md — promoted the live headless server to a
desktop window, un-fixing #11935 on exactly the launch shape this PR legitimizes.
findServeSubcommandIndex consumed a flag's value unconditionally while the
rewrite consumed it only when the next token was not flag-shaped. The two could
disagree and swallow the `serve` token, leaving `--serve` uninjected: #12677
again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now
share one definition of value consumption.
`<binary> serve --help` / `serve help` bound a network-exposed runtime server
with pairing on and printed nothing; the AppImage redirect already routes those
three tokens to the CLI, so refuse them here too.
`--no-pairing=false` translated to `--serve-no-pairing` with the value dropped,
disabling pairing for an operator who asked for the opposite. The CLI reads its
serve booleans as `flags.get(name) === true`, so a boolean is now translated only
in its bare form and the `=` form rides through as the CLI treats it.
Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite,
covering both ends of the contract (serveOrcaApp and getServeOptions); a
source-text lock on the index.ts redirect/rewrite ordering, which reverted
silently green before; an exhaustive self-consistency property test; and the
real GUI launch argv shapes that must never enter serve mode.
---------
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(i18n): localize the status bar Resource Manager tooltip and remote-host count
The Resource Manager tooltip/aria label and the SSH segment's host count were
assembled from bare English literals inside helper functions, so they stayed
English under every non-English UI language while the labels around them
translated. Route them through the catalog with _one/_other plural keys and
whole-line messages (locales reorder and repunctuate the summary), and add
en/es/ja/ko/zh entries.
Root cause of the miss: audit-localization-coverage bailed on any ancestor
binary expression whose operator was not `+`, which hid every string under a
`cond && <JSX/>` guard or a `?? 'fallback'` — including this segment's
'Connecting…'. Only comparison operands are code, so keep `??`, `||` and
`&&` walking, and localize the four real strings that surfaced.
Co-authored-by: Orca <help@stably.ai>
* fix(status-bar): flag the space-scan tooltip row instead of matching its English text
The tooltip tinted a row with `line === 'Space scan ready'`, so routing that
copy through the catalog silently dropped the tint in every translated build.
Return `{ text, emphasized }` and let the segment read the flag.
Adopted from #12439 by @smwbev.
Co-authored-by: Evgenii <smwbev@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(status-bar): key Resource Manager tooltip rows by role instead of array index
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the
entire reattach payload so it could read the last header plus 5000 chars. On a
2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work —
~75% of what xterm then spends parsing the same bytes — and the miss case paid
it in full for a result that is always false.
Two changes, both matching existing in-tree precedent: bound the scan to a
256KB tail (as the kitty tracker already bounds its own scan), and strip via
the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop,
which is also faster in V8 because it copies spans rather than building a rope
per character.
2MB snapshot, header hit 17.5ms -> 0.80ms (22x)
2MB snapshot, miss 8.7ms -> 0.52ms (17x)
200KB snapshot, header hit 1.5ms -> 0.62ms (2.4x)
config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and
asserts every candidate agrees with the baseline before timing it. It also
records a negative result: porting the daemon mouse mirror's includes()
pre-filter to the kitty tracker makes reattach slower, because snapshots always
contain the introducer.
Adds guards for the two behaviours a future shortcut would silently break: a
CSI-split header must still match, and a header behind the tail bound must not.
Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned.
Co-authored-by: Orca <help@stably.ai>
Mouse events posted with CGEventPostToPid reach the target app with no
window association, so AppKit never routes the press to a view: hover
states fire but the control is never activated, and the mouseUp is
dropped outright when posted back-to-back. Post click events to the HID
event tap instead (as keyboard synthesis already does), pace them, and
stamp mouseEventClickState so multi-clicks register.
Synthetic clicks now also report verification unverified/synthetic_input
from the helper itself, matching the other synthetic actions.
* fix(terminal): fence daemon endpoint ownership
* fix(terminal): clean failed daemon PID claims
* fix(terminal): close daemon ownership review gaps
* test(daemon): release startup IPC in boot smoke
* test(daemon): mirror production stdio in boot smoke
* fix(daemon): exit after rpc shutdown cleanup
* fix(terminal): make the socket name the daemon endpoint authority
The reported failure was a live daemon hosting PTYs that nothing could
reach: terminals acknowledged input and never ran it, listings diverged
from reality, and restarting the app never helped because the detached
helper survived. The ownership fence added for it could not fire in the
sequence that produces the split brain.
libuv unlinks the pathname a server bound to when that server closes,
with no ownership check. A daemon that lost its endpoint name therefore
deleted whichever socket then sat at that path — including a live
replacement's — stranding a daemon that still hosted every session.
Bind a private same-directory name and hard-link it into place instead:
libuv can only ever unlink our own bind name, the exclusive link is a
kernel-enforced endpoint claim, and the canonical name is removed only
under an inode ownership check. The bind name replaces the basename
rather than extending it, so it cannot overflow sun_path.
killStaleDaemon removed the PID record unconditionally immediately
before every fork, so the exclusive PID claim was always uncontested at
bind time. It also unlinked a live daemon's endpoint whenever a connect
probe merely timed out, and treated a `ps` timeout as proof of PID
recycling. Now only positive evidence of a dead endpoint authorizes
reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon
that cannot be proven stopped keeps its record and endpoint while the
launcher refuses to fork beside it.
A daemon whose endpoint was taken over now retires itself, draining
rather than killing, so an unreachable orphan stops being permanent.
A repaired PID record re-derives entryPath, appVersion and the Linux
incarnation markers from the authenticated owner instead of dropping
them; without appVersion a healthy daemon read as a permanently stale
bundle and, on Windows, went unpinned against daemon-host pruning.
Repair failure now fails open — abandoning a healthy daemon over a pid
file write cost every persistent terminal on the machine.
Also: treat only ENOENT as an unclaimed record so a Windows file lock is
not reported as an ownership conflict; settle start() before close() so
an accepted connection cannot defer it forever; sweep abandoned claim
and bind names; and type the endpoint-identity seam so a rename cannot
silently disable the fence.
Adds a real-process handover smoke that reproduces the failure with two
daemons racing one endpoint, and wires it into the native-smoke job.
* fix(daemon): retire only on proven endpoint ownership loss
The ownership watchdog read a null identity for any stat failure, so a
transient EACCES or EIO on the runtime directory would retire a daemon
that was still serving every terminal on the machine. Distinguish "the
entry is gone" from "the probe failed" and act only on the former.
Also require the loss to persist across two polls: a replacement
publishes by unlink-then-link, and a single observation can land in that
gap.
* fix(daemon): source repaired ownership metadata from the authenticated hello
Adversarial review found three defects in the previous two commits.
Re-deriving entryPath from the owner's command line truncated it at the
first space. A command line is a single space-joined string, so
`C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back
as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats
a present entryPath as authoritative, so a healthy daemon read as
`different_app_path` and was killed and re-forked — worse than the
missing-metadata case the derivation was added to fix. Carry entryPath
and appVersion as optional fields on the daemon hello identity instead:
the daemon already has both from its own argv, and per
docs/reference/remote-wire-compatibility.md a new optional field is safe
because every reader falls back when it is absent. This also removes a
synchronous `ps` spawn from the Electron main thread during startup.
`start()` rolled back the PID record even when it never published one.
Losing the endpoint link now runs that path, and the ownership-checked
unlink briefly renames the incumbent's record aside — enough to strand a
live daemon's ownership. Roll back only what we actually wrote.
publishDaemonSocketPath read its identity from the canonical name after
linking, so a concurrent unlink returned null: no ownership watchdog and
no endpoint cleanup on any shutdown path. Read it from the bound name
before linking, which shares the inode.
Refusing to fork beside an unconfirmed daemon left the user with no
daemon at all and no in-app recovery, since restart re-entered the same
fence. We have just proved something answers the endpoint, so adopt it
in degraded mode: live sessions keep working, fresh terminals run
locally. SIGTERM is also individually guarded now — an EPERM fell into
the blanket catch and reported "nothing alive", authorizing the very
duplicate this fence exists to prevent.
Also reset the ownership-loss streak on an inconclusive probe so the
confirmations are consecutive, and sweep scratch names before the launch
so a failed launch still reclaims them.
* 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).
* 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.
#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.
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.
STA-1716 reported that a packaged `orca serve` could become the single-instance owner after the desktop app exits, leaving Dock/Finder unable to restore a window — and that forcing a reopen made the headless process hydrate a renderer that interrupted and DUPLICATED live agent sessions.
Verification against main found every criterion already fixed (#8646 for desktop promotion and the fail-closed CLI, #12212 for duplicate serve activation, #12574 + #9729 for the resume/ownership guards). The genuine gap was criterion 6: the ticket's own automated regression never existed. An existing reliability gate asserted PTY identity survives promotion, but nothing asserted what the incident was actually about — how many agents the promoted renderer resumes.
This adds that coverage: a unit/service-level journey that drives the real single-instance lock, activation gate, settle and focus paths, then runs the real resume logic against a store seeded as a renderer freshly mounted inside the serve process, asserting zero duplicate resumes.
`settleServeDesktopActivation` moved from `index.ts` into its own module with identical semantics, so the test drives the real decision rather than re-implementing it — the earlier repro had to mirror that logic locally, which is the "test passes without running the scenario" failure mode.
Proven to be a real oracle: breaking each guard individually turns it red, and reverting the pre-#12574 pane form reproduces the incident exactly (two duplicate `codex resume` tabs).
Reconnect could never recover a terminal pane whose host-side process was gone (host restarted, or the workspace was never opened there): recovery only polled the tab inventory, which can never create the surface it is waiting for, so Reconnect spun for ~60s and gave up permanently.
Verified with a deterministic reproduction: on main the recovery path issues 51 inventory polls and zero activations across both an automatic online trigger and a manual Reconnect click; with this change the pane re-materializes, rebinds and accepts input.
Review found and fixed three further defects beyond the original change:
- an activation answered with a stale ready handle left the loop polling forever instead of re-activating;
- a non-missing activation failure (e.g. an older host without the method) never fell back to inventory;
- host-side, activating a parked surface permanently deleted the host tab, because an already-absent persisted binding was read as a competing owner *after* the destructive retirement had already run.
Independent review confirmed by mutation testing that every production change is covered by a test that fails when it is reverted, that only an authoritative inventory can retire a pane, that the loop is bounded under every failure mode, and that the unknown-liveness guard (proven death required before retirement) is intact.
Fixes STA-3002.
* fix(status-bar): invalidate the CLI session count on kill and restart
`pty:management:killOne` / `killAll` / `restart` tear sessions down via `adapter.shutdown()` and broadcast nothing — unlike `pty:kill`, which ends in `sendPtyExitToRenderer`. The status-bar count is an event-sourced cache, so killing sessions from Manage Sessions or "Kill all terminals" left the `>_ N` chip frozen until the popover was opened, which itself triggers a refresh.
> [!NOTE]
> The dual-source split described in the issue text was already fixed by merged #9387. This closes a *different* remaining invalidation gap that produces the same reported symptom.
Broadcast the teardown so the chip updates without needing the popover opened.
Fixes#8372
Co-authored-by: Orca <help@stably.ai>
* test(e2e): add recordable proof for status-bar-cli-session-count
Fails on origin/main, passes on this branch.
Test: drops after Manage Sessions kills a foreign daemon session, popover never opened
Co-authored-by: Orca <help@stably.ai>
* fix(status-bar): avoid duplicate inventory refresh after kill all
---------
Co-authored-by: Orca <help@stably.ai>
The number answers "which build of 1.4.163 is this", so carrying it across
versions made it meaningless — 1.4.164 opened at 38 for no reason a reader
could see. It now counts titles matching the base version being built, so a
version bump restarts the series at 01.
Deriving it moves from workflow jq into the script, because the number depends
on the base version and only the script knows which base the published tags
resolved to.
Timestamps go from `07-31 13:54` to `Jul 31, 1:54PM`, still Pacific.
Co-authored-by: Orca <help@stably.ai>
The build-time repair layer carried overrides that rewrite already-correct CJK
values back to English. Most are inert against today's catalogs but fire on the
next regeneration, so they read as latent regressions rather than policy:
- zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/
Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling
option, and ko/ja/es, stay translated — half a Chinese picker.
- The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival
的 form but replaced it with English instead of the bare color noun.
- ja `Play` was pinned to English though the catalog already reads 再生.
- A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress",
including the button that opens an MCP config file. "Open" is a verb (打开)
on buttons and a state (开放) beside 已关闭, so no single mapping fits.
Catalog corrections in the same area:
- The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh
while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 /
已关闭 on the sibling entry.
- The terminal cursor-color group is the on-screen cursor, not the Cursor
editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor".
- Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as
colors; now 天蓝/锌灰, and ja 空 becomes 空色.
- The ko disk-usage heading was pinned to bare "Space" while its own
description says 저장 공간; both now use 저장 공간.
Two policy tests pinned the Play de-localization. They diagnosed the input
correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to
播放 / 再生 rather than English.
Destructive drift (localized -> English on regeneration) drops from 40 to 21
for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search
qualifiers, path and filename literals, and product names.
en.json carries ~190 keys per locale that the locale catalogs have not been
bootstrapped with yet, so every repair-locale-catalog run threw a TypeError
before doing any work. Skip missing leaves instead.
Split out of #11728 so the crash fix can land without the catalog
regeneration, which still needs native-speaker review.
Co-authored-by: Turtle-Hwan <turtlehwan@gmail.com>
Co-authored-by: Orca <help@stably.ai>
The override for SshTargetForm.137e88ce8d held a truncated relay-TTL sentence
ending mid-clause at "최대:", which renders "Timeout after disconnect
(seconds)" — the sibling key 55c56cf2c7 — not its own English source. #12192
only corrected the terminals token inside that wrong sentence. Point it at the
value ko.json already ships, so a catalog repair cannot overwrite the correct
string with the wrong one.
Fixes#12113.
shouldPreserveEnglishValue keyed on the English value, so any key whose source
string equalled a NEVER_TRANSLATE_VALUES entry was forced back to English on
every repair run — agent, commit, repo, terminal and Continue were all on that
list. Measured on a clean checkout: ko 279, ja 461, zh 1180, es 479 values
rewritten, the large majority destroying translator work.
17 generic terms move into locale-generic-ui-terms.mjs, and the brand revert now
skips a term's canonical rendering, so genuinely nonsensical forms (zh 回购,
ja/zh 端子, es Comprometerse) still fire while 터미널/커밋/エージェント survive.
Brand, path, and code tokens are untouched — MD -> 医学博士 and HEAD -> CABEZA
are why that list still earns its keep.
No catalog values change; every file is under config/scripts/.
Co-authored-by: AnddyAgudelo <44873492+AnddyAgudelo@users.noreply.github.com>
Nine ko values said something other than the English source. The riskiest is
SourceControl.6d7f2a47e5 "Discard folder", rendered as 폴더 삭제 ("delete
folder") next to a sibling delete-untracked action. Others: "Only branches Orca
named itself" read as "branches named Orca"; "staged changes" as 단계적
("phased"); "first-party cloud" as the mojibake 1方클라우드; "discard the
deletion" as "the deletion is deleted"; "Stage all changes" as a sentence
meaning "prepare"; and Recipes as 조리법 (cooking recipes).
EphemeralVmsPane.skillTitle is dropped from the PR's test and override — the
key was renamed to cloudVmSkillTitle on main, so the assertion would resolve to
undefined.
Co-authored-by: ShinSungkyu <kxu4583@naver.com>
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from
#10664 would have been reverted by the next catalog repair. ja carried the same
defect — 新規 reads as "create new" — and is corrected alongside.
The shared Automation tour copy was rendered without passing through
translate(), and the overlay surface hardcoded its default Next and Done
labels. Copy is keyed off the step id rather than its position, so inserting a
step ahead of them cannot shift the text onto the wrong step.
Co-authored-by: 5Hyeons <ohs2251@naver.com>
* fix(ports): keep the app responsive when security software slows process creation
Orca ran the workspace port scan's probe commands (lsof/ps on macOS,
netstat + powershell.exe on Windows) directly in the Electron main process.
libuv performs process creation inline on the calling event loop, which in
the main process is the browser UI thread, so an endpoint-security module
hooking CreateProcessW froze the whole window for the length of the spawn.
The same stall also produced a false diagnosis: the 4s command watchdog was
armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its
deadline had already passed by the time the command started. Every scan on a
hooked host reported a command timeout, tripping the 60s -> 5min backoff and
the "Port scanning is temporarily paused after a command timeout" banner even
though the commands themselves were healthy.
Probe commands now run on a lazily created, unref'd worker thread with FIFO
one-at-a-time dispatch, and the watchdog is armed after execFile returns so it
measures the command rather than the spawn. Node's own execFile timeout kill
(killed: true) is classified as a command timeout, keeping the backoff working
for genuine hangs. A scan that observes a stalled spawn skips its optional
metadata commands for that cycle, capping a hooked-host scan at roughly one
stall instead of three.
Closes#11161
* fix(ports): keep advertised URLs when a stalled spawn skips port metadata
Review follow-up on #11161. The stalled-spawn early return handed
scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed
attribution and reconcileAdvertisedUrls told the watcher each worktree's
listeners had vanished. shouldEvictAfterScan then deleted every cached
advertised URL and broadcast a removal event; those URLs are only ever
captured from live PTY output, so the dev-server link was gone until the
server restarted.
The scanners now report metadataAvailable, and reconciliation is skipped for
a scan that never gathered attribution evidence. The skip is also no longer
self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely
on the current scan's spawnMs made every port permanently external (Stop
refused with 'Only workspace-owned local processes can be stopped here.').
Metadata is now re-probed on the scan after a skip, matching what the comment
and test name already claimed.
Co-authored-by: Orca <help@stably.ai>
* test(windows): stop a temp-dir lock from failing the CLI launcher smoke test
The native launcher assertions passed on windows-latest, but teardown's
rmSync raced Windows' release of the image handle on the exe the test had
just executed and threw EPERM, failing the job.
Cleanup now retries and, on Windows only, tolerates a residual lock code
instead of reporting it as a launcher regression.
Co-authored-by: Orca <help@stably.ai>
* fix(ports): scope the metadata skip away from attribution-dependent scans
The metadata skip was a process-wide parity flag, so Stop and the
localhost-label allowlist could land on a degraded cycle and reject a
port the panel had just shown as workspace-owned. Give those callers an
explicit requireMetadata option, and carry the previous cycle's listener
metadata forward so a skipped background scan no longer republishes
workspace ports as external.
Also pin the watchdog ordering: the stall in the execution test was
shorter than the watchdog budget, so a watchdog armed before execFile
still passed.
* build: guard worker-thread entries against electron imports (#11161)
Electron's module is not registered on worker threads, so
require("electron") throws "Cannot find module 'electron'" inside a
main-process worker and kills it at startup (verified on Electron 43.1.0).
plain-node-entry-guard covered only forked plain-Node entries, so the five
worker entries relied on hand-written "must stay electron-free" comments.
The port-scan probe worker is one import away from
port-scan-command-client.ts, which deliberately contains require('electron').
A violation there fails closed at runtime while every unit test still passes,
because the client's require is try/caught on the main thread.
Covers stt-worker, warp-theme-parser-worker,
session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry
and port-scan-command-worker-entry. The scan is transitive over the emitted
chunk graph, so a shared chunk that reaches electron is caught too.
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* test(windows): retry teardown for main's duplicate-PATH launcher fixture
Main's new csc-compiled harness runs an exe from the temp tree, which is
exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists
for; its bare rmSync would report a teardown lock as a launcher failure.
Co-authored-by: Orca <help@stably.ai>
* test(ports): pin the packaged-asar worker entry path
resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
happy-dom holds each observer's internal callback in a WeakRef, so a GC pause
under sharded CI load permanently kills a still-connected observer. That is what
made the activity portal readiness test flake: once the observer died, no amount
of re-applying ready DOM could release the latch.
Co-authored-by: Orca <help@stably.ai>
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment
The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).
The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.
Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.
`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.
Closes#12046
* test(windows): track the launcher's own-environment marker
The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.
Co-authored-by: Orca <help@stably.ai>
* fix(windows): follow the host block's PATH spelling on sparse daemon env patches
Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.
Co-authored-by: Orca <help@stably.ai>
* fix(windows): resolve the live PATH spelling by block order, not casing
Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.
* chore: drop unrelated merge formatting
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions.
main's version only moves on `release:` commits, and stable patches are cut
from release branches that never merge back. On 2026-08-03 main read
1.4.165-rc.0 for twenty hours while 1.4.165, 1.4.166 and 1.4.167 all shipped,
so every hourly built in that window was stamped 1.4.165-hourly.* while
carrying code newer than 1.4.167 — and sorted below the stable its user was
already running.
Resolve the base from the main repo's published tags instead, taking the patch
above the highest shipped stable. package.json stays a floor for the case where
main leads the tags.
Co-authored-by: Orca <help@stably.ai>
gh repo create leaves the repo empty, and publishing a release creates a
git tag that needs a commit to point at. The adhoc build got all the way
through notarization and upload before failing on "Repository is empty".
Co-authored-by: Orca <help@stably.ai>