mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
4e058d4a52ea4653a5cf86fac271c8010334361e
8992
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4e058d4a52 |
Fix flaky CI tests by adding retry logic and increasing timeouts (#15635)
* Fix flaky CI tests by adding retry logic and increasing timeouts Add Electron launch retry for CI runners where startup wedges before reaching 'ready', with fresh profile per attempt to avoid mid-init state. Increase skill install lock timeout from 100ms to 5s to account for fsync cost plus retry duration on loaded CI runners. * shorten comments |
||
|
|
6e25a90085 |
fix(terminal): keep a quick command queued until its own spawn takes it (#15630)
* Increase shell readiness timeout to match daemon barrier
Slow interactive rc files can take longer than 1.5s to initialize. Raise
the startup command readiness timeout from 1.5s to 15s to match the daemon
barrier and prevent queued commands from executing mid-startup.
* fix(terminal): keep a quick command queued until its own spawn takes it (STA-4876)
Triggering a quick command opened a terminal tab titled with the command's
label, the shell started and drew its prompt, and the command never ran.
TerminalPane snapshots `pendingStartupByTabId[tabId]` in a useState lazy
initializer, and a mount effect deleted the entry immediately. The pane's key
is `${tab.id}-${tab.generation ?? 0}`, so anything that bumps generation before
the command reaches a shell — the stall-recovery remount fired from
`requestTerminalPaneRecovery`, or the allDead activation regeneration — mounted
a second pane that re-read an emptied slot and spawned with no command at all.
The loss was permanent, which is why every scope failed alike: repo, global and
agent-prompt all funnel through the same queue-then-snapshot sequence.
Spend the entry at `onPtySpawn` instead, which is the one point that proves this
pane's own fresh spawn exists. A pane retired mid-connect never reaches it, so
the command stays queued for the next mount; reattach skips `onPtySpawn`, so it
cannot spend a command it never delivers.
Three details are load-bearing:
- Ownership is reference identity (`paneOwnsQueuedStartup`). Setup and issue
splits borrow the same `deps.startup` field for their own one-shot payload, and
that payload can be structurally identical to the queued command, so a
truthiness test would let a split pane spend a command it never runs.
- The consume runs after `bindActivePanePty`. While the tab still has no ptyId,
the queued entry is the only thing holding its worktree out of the
retention-budget force-park, so dropping it first unmounts the pane mid-spawn.
- The callback is one-shot. `onPtySpawn` fires on every fresh spawn a pane makes,
including hibernation wake and the respawn ladder, and a command queued after
the first launch belongs to that later launch.
Known residual, documented at the call site: the consume tracks "a pty exists",
not "the command ran". Windows embeds short commands in the shell argv, so they
execute before the spawn resolves and a pane retired in that window re-delivers
on remount; on POSIX the write waits for shell-ready, so a pty that dies in that
window loses a command already spent. Closing either needs a delivery signal
from main rather than this callback. Both windows are narrow, and both are
strictly better than losing the command unconditionally.
* fix(terminal): guard the queued-startup wiring the review found untested
Follow-ups from the final review pass on this branch.
- Collapse the ownership + one-shot decision into `createQueuedStartupConsumer`
so the call site is a single call rather than inline logic no test could
reach. Two mutants survived the whole suite before this: relaxing ownership to
a truthiness check, and dropping the one-shot guard. Both now fail.
- Rewrite the throwing-consume test. It asserted `updateTabPtyId` had been
called, which runs *before* the callback, so it passed with the try/catch
deleted. It now asserts the throw does not escape into the connect promise,
which is the invariant the try/catch actually provides.
- Correct the `onQueuedStartupSpawned` docblock. It claimed the callback is "the
first moment the command is guaranteed to reach a shell"; the diff's own caveat
says otherwise, since Windows runs an argv-embedded command before this fires
and a POSIX shell can die before the shell-ready write. It marks a live shell,
not delivery.
No behavior change: the consumer is the same predicate and the same one-shot,
moved behind one exported seam.
* fix(terminal): roll shell wrapper isolation into a fresh daemon
* Revert "Increase shell readiness timeout to match daemon barrier"
This reverts commit
|
||
|
|
e41074cb1f |
docs(readme): add Android install guide link to README.ko.md (STA-4817) (#15566)
Taken from #15396. Matches the English README's install-guide link added in #14978. Fixes #15395 Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: erishforG <eric.signal@kakaocorp.com> |
||
|
|
d8e9fa1bb9 |
Revert "fix(terminal): apply pane padding on all four edges (#15544)" (#15623)
This reverts commit
|
||
|
|
0f26ff4ad8 | Update README downloads badge | ||
|
|
feaebabf2c |
test(terminal): pin the two conditions that keep macOS period substitution out (#15393)
macOS rewrites a double space into ". " and hands the period to the pty.
Orca is not immune to that: a plain Chromium textarea in the Electron
version pinned here does substitute, measured on hardware with real key
events, and spellcheck="false" does not prevent it. What prevents it is
that the forwarder claims a plain space keydown and then empties the
helper textarea, so the text system never sees the word preceding the
space.
Neither half was a decision. Before
|
||
|
|
c92f394cde |
fix(pty): delete the reply-withholding scheduler (#15578)
* fix(pty): answer a terminal colour query in its own turn Root-cause follow-up to #15559, which stopped a CPR overtaking a deferred colour reply but left the deferral itself in place. Orca answers terminal queries by writing to the PTY master, which a line discipline in ECHO copies straight back out as junk on a cooked prompt (#12112). The guard was to withhold the write until an `stty` subprocess proved ECHO clear — and forking is what forced the decision to be async. Any deferral, however short, lets a reply written later in the same turn overtake this one, so the async probe was the bug's root cause. Read the bit synchronously instead. Linux and the BSDs redirect a master's mode ioctls to the slave, so a `tcgetattr` on the master fd node-pty already owns answers for the slave with no fork: measured 0.26us against 2403us for the subprocess. With a verdict available inline, a querying program that already cleared ECHO — every raw-mode prober, including the colour probe behind the `gh auth login` report — is answered in its own turn and can never be reordered. The deferral stays for the genuinely cooked case, and the ordering guarantee stays underneath it: hosts whose node-pty predates this patch get no sync probe and fall back to the deferred path, which mixed client/host versions make a live production path. Reply routing is all-or-nothing: a payload needing neither containment nor ordering stays on the host's own path, so a CPR answered during shell startup cannot pass the daemon's post-ready flush gate and splice into the buffered startup command. Native side is fail-safe: a kernel that did not redirect would answer from the master's own termios, whose ECHO defaults set, so the degraded verdict is "echoing" — never a false "quiet". The JS half ships in the pnpm patch while the binding needs a source build, so ORCA_REQUIRE_NODE_PTY_ECHO_STATE=1 makes CI fail rather than silently skip when it is handed an upstream prebuild. Co-authored-by: Brennan <brennanb2025@users.noreply.github.com> * fix(pty): keep the flush ordered under synchronous re-entry Three defects found in external review of the reply-ordering work. node-pty delivers onData inside the master write, so a query can be answered while the queue is mid-flush. `flushPendingWrites` spliced the array off before writing, so that reply saw an empty queue, took the same-turn path, and landed ahead of entries the loop had not written yet — reproduced as 01, 99, 02, 03. It now shifts one entry at a time so a re-entrant reply queues behind the rest, bounded by the length at entry so a re-entrant push cannot spin the loop. An overflow flush can re-enter as far as teardown. `answer` did not re-check `closed` afterwards, so it queued behind a closed delivery, returned true, and the reply was never written and never reported. The payload router's ownership comment overstated its guarantee. The `any` semantics are deliberate — returning false after a constituent was already written would have the caller re-write the whole payload and duplicate it into the child's stdin — so the residual mixed-failure drop is now documented rather than implied away. * fix(pty): delete the reply-withholding scheduler Orca answered a terminal query by withholding the write until a probe proved the slave's ECHO bit was clear. That was the wrong mechanism, and it is now gone: replies are written in the caller's turn and their echo is contained on the output side, where it always was. Withholding never removed an echo. The wait was bounded and always ended in a write, so the output-side projections were doing the work the whole time — including the readline rewrite, which happens with the tty already raw and which therefore no reading of the ECHO bit can predict. What withholding did add was an asynchronous write path, and that is what let one reply overtake another and land in the next program's stdin (#15559), what produced a re-entrancy inversion inside its own flush, and what four rounds of regressions have lived in. The last thing it covered was the verbatim echo of a `stty -echoctl` tty. That shape is now projected directly. It starts with ESC, so it is matched only when complete and never held as a partial: holding it would take a bare trailing ESC from the query parser and an expired hold would release it raw, so a query torn at its own ESC would never be answered. Complete-match-only is what makes the shape safe to project at all. Measured on a real pty: a cooked-mode master write is both echoed AND delivered — ECHO copies the bytes without consuming them from the slave's input queue, so a program arming raw mode with TCSANOW/TCSADRAIN (libuv's setRawMode, hence every Node agent) still reads them. Only a TCSAFLUSH switcher discards it, which it does on every terminal, none of which gates a reply on termios state. Deletes the pending-write queue, the async stty probe, the poll budget and probe rate limit, the deadline-driven flush, and the answer/ answerInOrder split. Replies now leave in call order by construction. No packaging, native or CI surface is touched. * test(pty): restore stty-probe coverage and pin the duplicate-query retry Archaeology on how withholding got here, and what its tests were really protecting. Deleting the ECHO probe took four tests with it that were not about the probe at all: they cover createSttyProbe, which the shell-readiness line-editor probe still uses — in-flight sharing, the per-platform stty flag, and transient-versus-permanent failure latching. Restored against the line-editor probe, which is now their only caller. Also pins the property that answers the one case an immediate write cannot serve. A program that queries while cooked and then arms raw mode with TCSAFLUSH discards the reply with the rest of its input queue. Nothing can prevent that from the terminal side, and no terminal tries. What matters is that such a program re-queries after its own timeout: the ingress declines to answer an already-answered slot but forwards the duplicate downstream, so the renderer's emulator answers the retry, by which point the program is raw. The retry path is the recovery, not withholding. * ci(pty): keep the fish real-PTY test in the shell-contracts lane only Reverting pr.yml to main dropped the exclusion for the fish query-reply test, which this branch keeps, so it would have run in the sharded lane as well. Restores it to the shell-contracts include list and the shard exclude list, and drops the parallelism expectations for the deleted cooked-querier suite and the echo-state env guard. --------- Co-authored-by: Brennan <brennanb2025@users.noreply.github.com> |
||
|
|
21b66197be |
fix(worktrees): cover WSL distro history and bound the retirement backfill scan (STA-4472, STA-4473) (#14924)
* fix(worktrees): cover WSL distro history and bound the retirement backfill scan (STA-4472, STA-4473)
* fix(worktrees): bound outstanding retirement backfill listings, not just their rate
The scan deadline abandons a listing, it cannot cancel one: an unabortable readdir
keeps its libuv threadpool thread until the OS releases it. The failure backoff paced
retries but never counted the abandoned calls, so a mount that stayed wedged stacked a
new stuck thread on every lapse until the four-thread pool starved every other
filesystem user in the process. UNC reads were already bounded by the WSL gate's
permit accounting; plain SMB/NFS listings were not.
Cap the outstanding listings process-wide and serve the memoized failure once the cap
is reached. Recovery is preserved: a slot frees as soon as an abandoned listing settles.
* fix(worktrees): keep a late retirement listing, and stop deferrals penalising healthy repos
Three review findings on the scan bound:
- A listing that landed after the 15s deadline had its result discarded. Under the WSL gate
that is the common case, not an edge one: the gate admits a single scan at a time and allows
it 60s, four times this deadline. The namespace was then left unseeded on exactly the mounts
this feature exists to cover. The answer is now kept when it arrives.
- A namespace deferred at the outstanding-listing cap never touched the wedged mount, so arming
its backoff spread one bad mount's outage to repos on healthy disks. Deferrals no longer
memoize; the next create retries as soon as a slot frees.
- The cap comment claimed it bounds threadpool starvation. It bounds this module's share only;
the WSL gate and its close lane hold threads of their own. Comment corrected.
Also stub the WSL resolver in the UNC-root unit test, which otherwise shells out to wsl.exe and
boots the developer's distro on a Windows runner.
* fix(worktrees): only count stuck retirement listings, and fence the backoff on a monotonic clock
Two more review findings on the outstanding-listing cap:
- The cap counted every in-flight listing, not just the stuck ones, so it fired on healthy
machines. Nested workspaces give each repo its own scan key, so a few first-time backfills are
routinely in flight together; the third was rejected and its create then picked a name against
an unseeded registry. Only listings that have outlived their deadline occupy a slot now, which
is what the cap was always meant to bound — healthy scans finish in milliseconds.
- The backoff fence compared wall-clock times. The WSL gate this scan runs under deliberately
avoids wall time for exactly this ('would misjudge stuckness across laptop sleep or NTP steps');
a backward step pinned a namespace in its failure memo for the size of the step. Now monotonic.
A third finding — that a late listing writing into an entry a retry has replaced loses the answer —
was investigated and refuted. An entry is only ever read back through the map, and callers hold the
promise rather than the entry, so a write to a replaced entry cannot be observed. No guard added:
the test for it passed with and without one.
* fix(worktrees): gate retirement rescans per namespace, and keep a partial answer usable
Replaces the process-wide listing budget with a per-namespace rule, and stops a refused source
throwing away the sources that did read.
- A global budget was the wrong shape: one wedged mount spends it on its own retries (lapse,
restack, lapse) and then every other namespace — including repos on healthy local disks — is
refused for the process lifetime, which is a strictly larger blast radius than the wedge it
replaced. A namespace now simply may not start a second listing while its own is still stuck,
so a bad mount costs exactly one thread and nothing else is affected.
- Rethrowing a gate refusal abandoned the whole scan at the first source. For a WSL repo the UNC
workspace root is listed first, so a stuck 9P route also discarded the plain, readable
Windows-side bucket scan that needs no distro access — worse than the behaviour before the
split. Discovery now returns what it read plus a "complete" flag; the names are used, and only
the memoization is withheld so the hole is retried.
- An I/O failure was reported as a complete empty listing, so a transient EIO on a redirected or
network home memoized "nothing is retired" for the process lifetime. Only ENOENT/ENOTDIR now
count as a complete answer.
The recovery tests now release the stalled listing rather than leaving it pending forever: a
retry while the previous call is still stuck is precisely what stacks threads.
* fix(worktrees): keep the scan retryable when a WSL distro home will not resolve
Resolving a distro home shells out to wsl.exe, which returns nothing for a stopped or slow distro
(the call has a 5s timeout) or when wsl.exe is not resolvable from the Electron process. That case
dropped the distro bucket source silently and still reported the scan complete, so the empty answer
was memoized for the whole process lifetime.
That is the STA-4472 defect re-entering through the back door: the distro is exactly where a WSL
workspace's agent history lives, so a workspace whose directory is gone leaves its only surviving
evidence unread, and the next generated create reissues that cwd. It does not self-heal either —
the scan key is derived from the probe path, which is unchanged by a failed home resolution.
An unresolved distro now marks the scan incomplete, which serves the names that were found while
leaving the hole to be retried after the backoff.
* fix(worktrees): trace a WSL repo's Windows-side workspaces into the distro too
Distro discovery keyed only on the workspace root being a UNC path, but a WSL repo can legitimately
own workspaces under C:\. computeWorkspaceRootAsync mirrors the workspace dir into the distro only
when the distro home resolves at create time; when that wsl.exe call fails it falls back to the
drive path, and those workspaces stay on the Windows side.
The agent is still spawned through wsl.exe, so its cwd is the drvfs mirror (/mnt/c/...) and its
bucket lands in the distro's own ~/.claude/projects, where the host-home scan cannot see it. The
scan then reported complete and memoized the empty answer, so the name was reissued and the next
occupant inherited the previous conversation — the STA-4472 defect, in the one configuration the
UNC check does not cover.
Which distro to look in comes from the repo path rather than the workspace root, since that is what
still identifies the distro once the root is a drive path.
* revert drvfs-mirror discovery, and pin the "no agent state" classification
Reverts the previous commit. The drvfs branch traced a WSL repo's Windows-side workspaces into the
distro, but its production wiring cannot be pinned: the distro comes from parseWslPath(repo.path),
which short-circuits off win32, so no assertion on a Linux or macOS runner can reach it — deleting
the wiring line left every test green. Shipping an unpinnable branch is the exact unreached-module
shape this PR exists to close, and it is not worth it here: the branch only pays off in a narrow
race where getWslHomeAsync fails while the workspace root is computed and then succeeds seconds
later during discovery. Whenever the distro home resolves, the root is UNC and the existing path
already covers it; whenever it does not, the scan is already reported incomplete and retried.
Also adds the missing guard on the other side of the same classification: ENOENT and ENOTDIR mean
"no agent state on this machine", which is a complete answer. That is the common case for a fresh
or Codex-only install, and misclassifying it as incomplete would turn the one-time seed into a
60s-interval rescan for the life of the process. The expression had no test; it does now.
* test(worktrees): make the retirement backoff window a real assertion
The test that claimed to cover it settled the stalled listing and re-entered in the same tick, so
outstanding cleared only in a later microtask and the no-restack rule answered first. It was a duplicate
of the test above it, and the backoff clause it was meant to pin had no coverage at all: deleting
the clause left all twelve tests in both files green, so a regression that re-probes a wedged mount
on every generated create would have shipped.
Flush the microtask so the listing is genuinely settled, then assert both directions — the memo
still serves the failure inside the window, and the same call succeeds once the window lapses.
* fix(worktrees): stop trusting a UNC ENOENT, which is what a shut-down distro looks like
Windows reports an unreachable 9P route as ENOENT, so a distro that has merely been shut down is
indistinguishable from one that never held any buckets. wsl.ts already refuses to trust a UNC
ENOENT for the same reason, probing inside the distro instead.
The classification added earlier called ENOENT a complete answer, which is right for a local home
that simply has no agent state but wrong here. After a wsl --shutdown the cached distro home still
resolves, so nothing else marked the scan incomplete: the empty result was memoized for the whole
process lifetime and every later generated create in that namespace reissued names spent inside
the distro. That is STA-4472 again, reached by a different route.
ENOENT now only means "absent" off UNC.
* fix(worktrees): tell an absent distro directory apart from an unreachable 9P route
Distrusting every UNC ENOENT fixed the shut-down-distro hole but overshot: a distro where nobody
has run Claude genuinely has no ~/.claude/projects, which is the common case for Codex-only users
and for anyone running agents from the Windows side. Those namespaces could never report a
complete answer, so the one-time seed became a full rescan every 60s for the life of the process —
each one re-spawning wsl.exe and taking the single process-wide scan slot from transcript
discovery, on a path that runs on every composer open rather than only at create.
Probe the parent instead. If it lists, the child really is absent and the answer is complete; if it
does not, the route is down and the scan stays retryable. Both directions are pinned: reverting to
either of the previous behaviours turns a test red.
* fix(worktrees): walk up to a reachable ancestor, not just one level
The reachability probe checked a single parent, which only disambiguates when ~/.claude exists but
~/.claude/projects does not. The far more common shapes have the ancestors missing too: a distro
where Claude has never run has no ~/.claude at all, and a repo with no workspaces yet has neither
the workspace root nor its parent. In both, the one-level probe also got ENOENT and called the
route unreachable, which is exactly the 60s rescan loop it was added to prevent.
Walk up until a listing succeeds, bounded so a pathological path cannot hold the scan slot. One
reachable ancestor proves the route is up, so the ENOENT below it is real absence.
The test that was supposed to guard this had ~/.claude resolving, so the real shape was never
exercised — which is why the defect shipped green. Its fixture now leaves the whole chain absent
up to the distro home, and reverting to the one-level probe turns it red.
* docs(worktrees): record what gating retirement listings costs
The shared WSL filesystem gate admits one scan task process-wide, and its stuck-task check matches
scan against scan regardless of route. So retirement discovery now queues with — and on a wedged
distro can fast-fail — native-chat transcript discovery, which the ungated readdir it replaced
never could.
Gating is still right: the gate holds the only deadline and permit accounting these UNC reads get,
and without it a hung 9P route keeps a libuv thread outright. A dedicated lane would need a third
priority, which is a change to the gate rather than to this file. Writing the trade-off down so the
next reader does not have to rediscover it.
* perf(worktrees): probe reachability with stat, and record that the gate coupling runs both ways
The ancestor probe only asks whether a directory is there, but it listed it — enumerating a WSL
home over 9P, on the composer-open path, holding the single scan permit while it did. stat answers
the same question; the gate already supports the operation.
Also corrects the trade-off note added last commit, which recorded only the direction where
retirement discovery is the victim. Because the gate stuck-check matches scan against scan
regardless of route, the reverse is now true too and is the part this PR introduces: a retirement
listing wedged on one distro can fast-fail transcript discovery on a healthy one.
* fix(worktrees): drop imports the discovery extraction left unused
The rebase onto main kept main's import block, which still pulled readdir and
homedir for discovery code this branch moved into worktree-retirement-discovery.ts.
* fix(worktrees): drop the last import the discovery extraction left unused
|
||
|
|
3e079debec |
fix(sidebar): host-qualify discovery notice rows on multi-host projects (#15546)
* fix(sidebar): host-qualify discovery notice rows and collapse one checkout's twins A project checked out on several hosts emits one discovery-notice row per checkout, and those rows only named the project. A sidebar with paired remote hosts therefore showed several identical "N hidden worktrees" buttons under one project header, with no way to tell which machine each belonged to — or that one of them was another machine's worktree inbox entirely. Two causes, both fixed here: - Notice rows carried no host context, unlike worktree rows, which have been host-labelled since STA-4343. Both notice rows now take a host label, applied per project (not per rendered section, since a card can land in the pinned fallback) and only when that project spans hosts. The label also lands in the review, expand, and dismiss accessible names, so the actions that write to a specific host's repo record say which host that is. - One machine registered as a direct SSH target *and* paired as a runtime environment gives a single on-disk checkout two repo records with independent hidden-worktree state, so it emitted two rows for one directory. Repos now resolve to a (hostname, path) checkout key, and twins collapse to the record this client persists itself — its visibility state is the user's own and survives the paired runtime going away. The key is deliberately conservative: an unresolved hostname, or a tunnelled environment answering on loopback, yields no key and never collapses anything. Renderer-only; no wire or persistence change. * fix(sidebar): drop the machine-identity collapse, gate notice labels on host ids Replaces this branch's second change after a plan review found it has no precedent and eight concrete failure modes. Deleted: the (hostname, path) checkout key that collapsed two repo records believed to be one machine. Orca models a direct SSH target and a paired runtime environment as different execution hosts everywhere else; that change asserted sameness by resolving strings a user typed in two places. It also dropped rows (a differing count vanished with the shadowed record), flipped with the sidebar host filter, ignored port and user so a host and a container on it could merge, tie-broke on repo-store order, was disabled in the one case Orca can prove (a tunnelled pairing answers on loopback) and fired only on coincidence, and left the visibility dialog showing state the sidebar had hidden. Its module also carried a literal NUL byte, so git classified the file as binary and the diff was unreviewable. Kept, with two corrections: notice rows still carry a host label, but the gate now counts distinct host ids rather than distinct label strings — two hosts sharing one user-facing label is exactly when the rows are hardest to tell apart — and membership is read from the unfiltered repo universe rather than the host-filtered notice candidates, so a label no longer appears and disappears with the filter. Two hosts that share a label still render the same label. Disambiguating that is a shared concern across worktree badges, host headers, and host-filter options, and needs its own design; three verification passes each found a different hole in doing it here. Follow-ups: general host-label collision, and the repo-record duplication that produces the twin rows in the first place. * fix(i18n): catalog notice host scope copy * feat(sidebar): show each notice row's host with the project-on-host glyph Notice rows on a multi-host project already carried a host label, but two hosts can share one user-facing name, and the label truncates first in a narrow sidebar. Each row now also carries its host's glyph. Deliberately the same indicator worktree cards use (worktree-card-header): a Server glyph, ServerOff when a paired runtime has no live status, and a "Project on ..." tooltip naming the host — SSH and runtime keep their distinct tooltip wording. Local hosts draw nothing, as on the cards. The glyph is shrink-0, so unlike the text label it survives the sidebar narrowing, and the row keeps an identifying mark either way. Rows now carry the host id alongside the label, since the label alone cannot select a glyph or its tooltip. Catalog entries for the new copy ship with the change rather than relying on inline fallbacks. * refactor(sidebar): draw notice-row hosts with the shared host glyph Follow-up to the notice-row host indicator: use the one glyph vocabulary the app already has instead of a second copy of it. HostRowIcon — a monitor for this computer, a server for anything remote — was private to the composer's run-target rows. Moved to a shared home and reused, so the sidebar and the composer cannot drift apart. The run-target module re-exports it, leaving its own call sites untouched. Every notice row now gets a glyph, local included, so no row is the odd one out; the tooltip still names the host and says when a paired runtime has no live status. Same size and tone tokens across kinds, so no row reads as decorated relative to its neighbours. * fix(sidebar): make notice host glyphs accessible |
||
|
|
4b2ed5ddd4 |
fix(terminal): apply pane padding on all four edges (#15544)
* fix(terminal): apply pane padding on all four edges Move the configured inset onto xterm so the terminal fills its pane while the fit calculation accounts for both sides of each axis. Add a geometry golden that forces cell remainders and verifies dynamic padding without relying on renderer pixels. * fix(terminal): normalize imported padding for fitting * fix(terminal): align stored and fitted padding |
||
|
|
5ca747dad0 |
docs(ssh): state the SSH execution boundary and pin the liveness vocabulary (#14971)
* docs(ssh): state the SSH execution boundary and pin the liveness vocabulary Nothing under docs/ described how work splits between the client and an SSH host, so agents and humans inferred it from error strings and got it wrong: loss of contact was repeatedly reported as process death, which orphaned live remote agents and cold-started duplicates over the same worktree. Pins the vocabulary to the incumbent live/unverifiable/exited verdict from unstopped-pty-verification so no synonym is introduced, records the one real discriminator (all of a host's terminals drop together on link loss; one alone means process exit), and lists the outstanding gaps with citations. Tracked via the docs allow-list and linked from AGENTS.md, per the convention in .gitignore. * docs(ssh): cite the live restoreRequired site after it moved The throw now lives in reattachSshPtySessionForSpawn; ssh-pty-provider.ts no longer contains it. Caught by the worker fixing it, against a newer main than the audit ran on. * docs(ssh): require host evidence for liveness verdicts * docs(ssh): keep boundary references stable * docs(ssh): fence liveness evidence to its host identity * docs(ssh): state replay and environment boundaries precisely * docs(ssh): correct replay and platform boundary claims * docs(ssh): describe headless runtime continuity accurately * docs(ssh): distinguish authority from client metadata * docs(ssh): describe pending fixes accurately * docs(ssh): date the gap list and name the PR that closes each entry The Known gaps section was accurate when written and becomes actively misleading as its fixes land: it told a reader to go fix restoreRequired, the missing unverifiable verdict, and the absent terminal-list host field, three things now addressed by #14974, #14977 and #14973. Mark the section as dated, require verification against current code before acting on any entry, name the PR per entry, and move landed items out. Also correct the two body claims that the landed fixes invalidated. The rules above are durable; only this section rots. * docs(ssh): make the boundary doc a durable ruleset, not an incident record The Known gaps section was 18 of 93 lines enumerating specific defects from one investigation, several already fixed by sibling PRs in the same batch. A reference doc that needs a 'this section rots' warning is telling you the section belongs somewhere else; those entries belong in issues. Replace the six-row table of currently-lying signals with the method that outlands any particular bug: ask whether the owning host produced the signal, whether every PTY on the target went quiet together, whether the termination event matches the current incarnation and generation, and whether a returned status is actually a claim. Same for artifacts - state what ls-remote and a PR head each do and do not prove, rather than listing which command is currently wrong. Nothing here goes stale when the open fixes land. |
||
|
|
2e895da937 |
fix(browser): name the requesting frame and the permission in denial notices (#15542)
* fix(browser): name the requesting frame and the permission in denial notices Two defects in the same notice, both found by the review of #15481 and left out of it deliberately. The notice named the wrong site. setPermissionRequestHandler passed webContents.getURL(), which is the top-level document, so a permission request from a cross-origin sub-frame was attributed to the embedder. Every PermissionRequest variant carries requestingUrl, so all three call sites now use it and fall back to the top-level document only when it is absent. The notice also showed raw Chromium permission names. humanizePermission mapped two permissions and returned the raw token for the rest. That now matters more: #15481 granted ordinary storage-access and left top-level-storage-access denied, making it the storage denial a user can still hit - rendered as its raw token. The default still returns the raw token. Inventing prose for a permission nobody has seen is worse than showing its real name. Does not change any permission verdict, and does not fix Google sign-in (#15221). * fix(browser): keep permission denial attribution accurate Capture fallback URLs before asynchronous media permission handling and treat opaque requesters as unknown rather than blaming the top-level page. Clarify permission descriptions and cover origin normalization, navigation races, and mapped copy. |
||
|
|
471bc9d8ce | Ship the WSL transcript helper with the Windows relay (STA-4831) (#15529) | ||
|
|
9d06b3ba93 |
ci: stop docs-only commits from starting the skill-roundtrip matrix (#15474)
A cancelled Skill update round trip on a README merge painted main red because push to main had no path filter. Share the PR path list on push, and skip expensive PR Checks when every changed file is docs. |
||
|
|
9a898a84cc |
fix(terminal): keep xterm's render pause latched for a pane with no layout box (#15555)
* fix(terminal): keep xterm's render pause latched for a pane with no layout box resetWebglTextureAtlas() released xterm's paused-render gate for every pane of a visible manager, including panes that are display:none (a collapsed sibling of an expanded pane, a restore that stays display:none for its whole reattach). forceRepaintThroughRenderPause exists for a pane that is already DOM-visible while xterm's IntersectionObserver lags a frame. On a pane with no box it paints the freshly cleared render model into nothing, and because the observer only fires on a state change it never re-pauses the service. It also clears _needsFullRefresh, which is the only thing that makes _handleIntersectionChange repaint on reveal and flush the deferred _pausedResizeTask. Latch instead for those panes: terminal.refresh() re-arms _needsFullRefresh and xterm repaints from it on reveal. * test(terminal): type the render-service repaint mock |
||
|
|
4daace6251 | fix(tabs): stop split workspaces multiplying columns on return (#15482) | ||
|
|
ef096d539d |
fix(terminal): refuse a cursor on a screen read, and correct the source docs (#15563)
Review follow-up on #15380. The RPC accepted `cursor` and `screen` together. The CLI refuses the pair, but terminal.read is reachable without it, and honoring both answered with rendered lines carrying the stream's pagination metadata — two frames of reference in one payload, which is the confusion `source` exists to remove. The guard beside it, withVisibleSnapshotFallback, already declines to substitute rendered lines when a cursor is present; the screen path now agrees, at the RPC boundary where every remote caller passes. Nothing could previously send both, since `screen` did not exist, so rejecting breaks no existing caller. The command notes and the runtime comment both still described the fallback as `source: stream`, left over from renaming that value to `screen-unavailable` during implementation. The spec text is surfaced through `orca help` and the agent-context schema, so a caller following it would test for a value the code never emits. Both now describe all four states, including that an absent source means the host predates the field. |
||
|
|
acbcb477a1 |
Auto e2e tests autofix scheduled ci 1h run 1 20260818T2143 (#15379)
* fix: update E2E tests for API changes and selector robustness - Improve source control file locator specificity to avoid flakiness - Fix board test to use correct worktree ID attribute - Update removeWorktree calls to pass host ID parameter - Simplify git status polling with timeout expectation * fix: increase packaged-watchdog launch timeout and await git-status rows Extract hardcoded 15s launch timeout to a 30s constant for better reliability under load. E2E test now waits for all git-status rows to render before asserting absence of status messages, preventing flaky passes when the list is still loading. |
||
|
|
c72a4eecdd |
refactor(shell): collapse the zsh wrapper to one .zshenv and a precmd hook (STA-4786) (#15391)
* refactor(shell): collapse the zsh wrapper to one .zshenv and a precmd hook (STA-4786)
Orca needs to run code after the user's own zsh startup files. It bought that by
keeping ZDOTDIR pointed at its own wrapper dir for the whole of startup and
sourcing each user file by hand -- four generated files per transport, with a
fake ZDOTDIR live while /etc/zshrc ran. That single decision is the root of a
whole bug family:
- /etc/zshrc assigns HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history unconditionally, so
history landed inside Orca's own dir (#11044), and an epilogue had to repair it.
- zsh's sourcehome() ignores ZDOTDIR once the shell is in sh/ksh emulation, so a
user .zshenv or .zprofile ending in `emulate sh` hid every later wrapper file.
The emulation degrade blocks and their forked $(emulate) probes exist for that.
- One wrapper dir shared by two installed builds could mix files from both, so
every generated file had to redefine the helpers it called.
- The baked generation-time ZDOTDIR literal is unusable when a Windows-generated
wrapper is sourced inside WSL via /mnt/c (#8003), so the runtime path had to be
re-derived from %x.
The wrapper now hands ZDOTDIR back on its first lines and defers Orca's work to a
precmd hook that runs at the first prompt -- after .zprofile, /etc/zshrc, .zshrc
and .zlogin, every one of which zsh reads from the user's own directory exactly
as in an unwrapped shell. Each bug above stops being reachable rather than being
repaired, and their machinery goes with them: eight of thirteen exported blocks in
shell-templates.ts, both drifted discovery bodies (unifying them closes the
"reconciling the two is a follow-up" note the file carried), and the relay's
separate ORCA_USER_ZDOTDIR shape. Generated zsh drops from 819 lines across
twelve files to 143 in three.
Two things the design has to get right, both found by running it rather than
reasoning about it:
- Every function is defined ABOVE the source of the user's .zshenv. A user file
ending in `emulate sh` puts the rest of the wrapper under sh parsing rules, and
the first prototype died there with `parse error near '\n'` -- silently, leaving
the pane unwrapped. Function bodies are parsed at definition time.
- ORCA_ORIG_ZDOTDIR is vetted, not trusted. The launch config only sets it when it
resolved a usable dir, but a pane inherits its parent's environment too, so a
stale value from an older build can arrive on its own and would point ZDOTDIR
back at a wrapper dir. The ownership check Node applies now also runs in the
shell, where that route is visible.
Orca also stops inventing a ZDOTDIR: where the user has none, ORCA_ORIG_ZDOTDIR is
absent and the pane ends with ZDOTDIR unset, as an unwrapped login zsh does.
Verified on real zsh over a real PTY -- necessary, because a precmd hook never runs
in a shell started with -c, so the existing `zsh -i -c` probes could not have
exercised this design at all. src/main/zsh-startup-hook-pty-harness.ts drives the
shell to a prompt and reports through a file rather than stdout, which a PTY echoes.
* test(shell): cover the relay variant of the zsh hook in a real shell
The relay writes its own variant -- no OSC 133, remote CLI bin dir instead of the
agent-teams shim -- and it had no live coverage. It used to carry a second ZDOTDIR
shape as well, which is how it drifted from the desktop template in the first
place; now the spec flags are the only difference, and this pins that.
* fix(shell): rebase the single-file hook onto content-addressed wrapper trees
#15285 landed content-addressed wrapper roots and a per-transport fileset module
while this branch was in flight. The fileset modules are now the single place the
tree is described, so 'only .zshenv' is stated once per transport and the
required-paths check follows from it rather than repeating the list.
* test(shell): point the mixed-build proof at the relay, the one fixed wrapper path
#15285 content-addressed the desktop and daemon trees, so two builds can no
longer write the same directory there and the scenario this file covers became
unreachable on those paths. The relay still writes a fixed ~/.orca-relay/
shell-ready, so that is where the hazard survives and where the proof belongs.
* fix(test): make the zsh PTY harness survive a startup that stops to ask
Two CI-only failures, both from driving a real PTY where the old probes drove a
pipe:
- A host whose global zshrc runs `compinit` over directories it considers
insecure stops startup and ASKS. A pipe-backed `zsh -i -c` never saw the
question; a PTY sits at it until the timeout. The harness now answers it.
ZSH_DISABLE_COMPFIX does not help -- that is an oh-my-zsh convention and plain
compinit ignores it, which I confirmed by reproducing the prompt locally.
- The PS1 line was typed at t=0, so on such a host the question consumed it as
its answer. The harness now waits for the shell to fall quiet first, which
also stops a slow prompt framework racing the same write.
Also merges a duplicate vitest import the native code-quality audit flagged.
* fix(test): stop the live-shell assertions assuming macOS host behaviour
Two of them hardcoded what my machine does rather than what Orca owes:
- LINEINIT was pinned to 'none'. A host whose global zsh config installs its own
zle-line-init widget has one either way; the contract is that it looks the same
wrapped as unwrapped, which the assertion beside it already states.
- The dropped-precmd_functions case asserted HISTFILE was no longer the scoped
path. Whether the scoped value survives at all is the host's call: macOS
/etc/zshrc overwrites HISTFILE so it does not, and a host with no such
assignment keeps whatever the spawn env set. Now compared against an unwrapped
pane given the same env, which is the real contract on both.
Also notes, where the emulation cases live, that they only discriminate on a host
whose system zshrc clobbers HISTFILE -- on CI's Ubuntu the load-bearing assertion
is ORCA_HISTFILE having been consumed.
* test(shell): re-pin the fixes the four-file wrapper was built for
Archaeology over the removed blocks: each existed for a bug, so each needs the
bug shown to be unreachable rather than just the code gone. Six restored or added,
each naming the change that introduced the behaviour.
- #8003, twice: the wrapper sourced from a relocated root, and from a non-ASCII
(token-range) one. The old file baked its generation-time path in and had to
re-derive the runtime one from %x to avoid using it; this one bakes nothing.
Both runs assert ORCA_SHELL_FEATURES came back consumed, so 'the user's .zshrc
loaded' cannot pass on a pane that never read the wrapper at all.
- #4667: user startup files must see their OWN ZDOTDIR while they run, or plugin
and theme lookups resolve into Orca's dir. The old wrapper swapped ZDOTDIR
around each source; this one never takes it away, and the values now have to
match an unwrapped pane's.
- #1947: a user .zshenv that returns early.
- #15258: an inherited ZDOTDIR that is an Orca wrapper dir must be refused. CI
proved this route is live -- the launch config only sets ORCA_ORIG_ZDOTDIR when
it resolved a usable dir, but a pane inherits its parent's environment too.
- #11044/#11146: a nested Orca inherits neither cross-process channel and no
ZDOTDIR of Orca's, which is what makes #11044's plain shape unreachable rather
than repaired. Verified the child-env probe detects a real leak before trusting
it to report the absence of one.
|
||
|
|
fdd4091ebd | fix(hooks): isolate lint-staged backups per worktree (#15388) | ||
|
|
d7a23c84a9 |
fix(pty): keep a CPR reply from overtaking a deferred colour reply (#15559)
A background-colour probe writes `OSC 11 ;? ST` then `CSI 6n` and reads exactly one response, using the CPR as its sentinel: a non-OSC first response means "unsupported" and it stops draining. #13309 routed live cooked-echo-risk replies through the ECHO-probe deferral while CPR kept the immediate path, so the CPR overtook the colour reply, the prober gave up, and the stray `ESC ]` was left in the tty for the next program — `gh auth login` died on it with an escape-sequence error. Queue a reply that needs no echo containment behind ones that do, FIFO, and only while something is actually deferred, so latency-critical replies stay immediate on every other path. Windows is unaffected: only posix-pty defers, so the queue is always empty there. Also: an in-flight echo probe is already the write continuation, so re-arming the timer for a queued reply would fork a second stty and throw away the first verdict; and teardown now hands queued uncontained writes to the pty best-effort instead of dropping bytes the caller was told were sent. Known scope limit, pinned by tests and tracked for follow-up: the guarantee is FIFO among recognised query replies, not over every byte — a reply coalesced with a keystroke, and ordinary typed input, still bypass the queue. Both were unordered before this change too. |
||
|
|
d541982b9c |
Improve cmd j search usefulness (#15551)
* Show last active time for workspace tabs in cmd+j palette Adds session-age formatting and activity tracking to help users find recently-used tabs. Replaces host badge display with last-active timestamps that reflect either agent activity or worktree PTY activity, whichever is more recent. * Improve cmd+j search ranking with direct fields and recency Prioritize results matching direct fields (titles, content) over container fields (worktree, branch, repo). Use tab focus time to break ranking ties. Makes search more useful for quick navigation. * Extract path flavor logic to cross-platform-path utility - Remove local pathFlavor function in favor of shared cross-platform utilities - Simplify buildExcludePathPrefixes to use relativePathInsideRoot and resolveRuntimePath - Ensures consistent path handling for both local and remote roots * Improve cmd+j search ranking with recency-based tiebreaking Track lastFocusedAt on tab creation/focus and use it to break ties between equally-ranked search results. This surfaces recently-used items first, improving search utility. Also fixes hasDirectHit to check field matches directly rather than evidence metadata. |
||
|
|
a76a95d111 |
fix(terminal): make remote-host take-back release the phone-fit lock (#15473)
* fix(terminal): make remote-host take-back release the phone-fit lock The remote-desktop branch of reclaimTerminalForDesktop was the one path that rolled the presence lock back when its reclaim resize did not converge. On a remote/SSH host that left the phone-fit banner stranded and made every subsequent "Take back all terminals" click a no-op. Its sibling (active mobile subscriber) released the lock but still reported the layout's `ok`, so the desktop renderer skipped its post-take-back refit and focus. Both now follow the guarantee the method already documents: an explicit desktop take-back always drops the lock, and the trailing remote layout is best-effort. * review: pin the driver flip and correct the applyMobileDisplayMode contract - Tighten the held-branch driver assertion to the exact post-release state so it pins releaseDesktopTakeBack's flip rather than merely "not mobile". - applyMobileDisplayMode's doc claimed reclaimTerminalForDesktop gates its transitions on the returned convergence flag. No branch does after this change; say so, so the gate is not reinstated. Hooks skipped (machine load); oxfmt --check and oxlint verified clean manually. * test(terminal): pin the converging remote take-back resize The two take-back tests both force the reclaim resize to fail, so nothing covered a take-back that converges. Deleting the `idle` driver flip left every suite green while applyRemoteDesktopLayout no-opped on a still-mobile driver — lock dropped, `true` returned, PTY stranded at the phone grid. |
||
|
|
59c5624e14 |
fix(terminal): require a proven exit before retiring a subscription's lease (#15470)
isPtyKnownExited read a PTY record as exited whenever `connected` was false:
if (pty) { return !pty.connected }
Its own leaf fallback, one line below, demands getTerminalState(leaf) === 'exited',
which is only true once lastExitCode is set. So the same function proves absence on
one path and infers it on the other, and the inferring path is the one that runs
whenever a record exists.
onPtyExit is the only writer of lastExitCode. The liveness sweep clears `connected`
with no exit code for every PTY behind a dropped relay, so that state is a lost
connection to a process that may still be running on the host — not an exit. Reading
it as one makes subscribeToPtyExit fire its listener synchronously at subscribe time,
which retires the lease and emits `end` for a live terminal. Mobile reads `end` as
"PTY gone" and rearms; after three attempts it stops and leaves the composer on
"Waiting for terminal…", which is where the 0.0.44 permanent lock comes from.
Use the runtime's existing three-valued discriminator so both paths demand the same
proof. 'unknown' now keeps watching, and the later real exit still fires the listener;
a subscription that outlives its PTY is still bounded by the connection abort.
Both callers are in subscribeToPtyExit — the subscribe-time fast path and the
registration-race re-check — and both want proven-exited, so neither changes shape.
|
||
|
|
6ac79ef0aa |
fix(workspaces): settle an orca.yaml trust prompt when the modal slot is taken (#15540)
* fix(workspaces): settle an orca.yaml trust prompt when the modal slot is taken The app has one modal slot, so any openModal/closeModal evicts whatever held it. A pending orca.yaml trust prompt owns the promise that quick create awaits, and eviction dropped its resolver: that submit never settled, and because the trust prompts are serialized on a module-global chain, every later create/remove in the session silently did nothing. Modal data can now carry an onModalDismissed callback that the slot invokes when it evicts an entry; the trust prompt uses it to resolve as 'skip', which is what dismissing the dialog already means. * fix(workspaces): make trust prompt settlement one-shot |
||
|
|
30bf2647fc |
fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it (#15472)
* fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it A socket close or response timeout rejects an in-flight worktree.create as delivery-unknown: the frame reached the wire, so the host may already have built the worktree. The client only replayed connection-migration cutovers, so every other ambiguity surfaced as a create failure for a create that may well have succeeded. Replay on the same clientMutationId — which the host already dedupes — after waiting for the transport to come back. * fix(mobile): bound the ambiguous worktree.create replay by the host's dedupe window The replay was bounded only by a retry count, but what makes a replay reconcile instead of building a second worktree is wall clock: the host drops a settled create's dedupe record 60s after it resolves, and past that the replay is just a fresh create that the host's suffix loop happily duplicates — for a folder workspace, into a second workspace with the very same name and no collision check at all. Two paths ran past that window: - The request-timeout path. A silently dropped response frame leaves the socket alive, so nothing rejects until WORKTREE_CREATE_TIMEOUT_MS — ten minutes, with no bound at all on when the host actually resolved. This was previously the path that replayed *soonest*, short-circuiting the reconnect wait because the transport still looked healthy. Invert it: every path that reports a real drop has already left 'connected' by the time the rejection surfaces, so still being 'connected' identifies the timeout and is now refused. - The reported-drop path. Worst-case detection is a full liveness idle period plus the missed-probe budget before the client even learns the socket is dead, and the old 20s wait on top of that overran the record. Derive the wait from the watchdog constants and the TTL instead of hardcoding it, and anchor a single deadline at the first ambiguity so a second wait gets the remainder rather than restarting. The TTL now has one definition shared by both processes, so the client asserts its budget against the host's real window instead of a copied literal. * fix(mobile): end the reconnect wait on a revoked pairing, and pin the wait's behavior waitForRpcClientReconnected resolves only on 'connected' or the timeout, but an 'auth-failed' client never reaches 'connected' — so a create interrupted by a revoked pairing sat out the full wait before surfacing the error it already had. Treat auth-failed as a terminal answer on both the fast path and the listener. The helper also shipped with no tests of its own: its already-connected fast path, its timeout path, and the synchronous-notification-during-subscribe teardown were only ever exercised indirectly through the retry suite, and neither RpcClient implementation notifies synchronously, so that branch had no coverage at all. Add a direct suite covering all of them, asserting listener and timer teardown rather than just the resolved value. Also give the fake-timer tests an explicit timeout. advanceTimersByTimeAsync yields through real macrotasks between ticks while vitest's own budget runs on real time, so on a loaded runner the default 5s is reachable — observed once as a spurious timeout in this suite. * fix(mobile): bound the ambiguous replay in wall clock, not timer time The replay window was derived from the liveness watchdog's own budget (idle + missed probes x probe timeout). That is a bound on how long the watchdog takes to *fire*, not on how much wall clock passed. iOS and Android suspend JS timers while the app is backgrounded, so across a background cycle the socket dies silently and the pending create rejects delivery-unknown minutes later with the timer-derived ceiling still reading ~44s. The replay then lands well past the host's 60s dedupe record and the suffix loop builds a SECOND worktree - for a folder workspace, one with the very same name and no collision check at all. Anchor the deadline on the watchdog's lastInboundAt instead: a wall-clock stamp of a frame that really arrived, so it stays honest across a suspension. Fall back to the send time when the transport can't vouch for one (relay sessions run with idleProbeMs: null), which errs toward refusing the replay. Also restore the delivery-unknown discrimination test that the still-connected guard had made vacuous, pin the still-connected guard itself against a live inbound stamp, and pin the deadline against being re-read from a fresher replacement session. |
||
|
|
36d78e88af |
fix(agent-hooks): stop Antigravity's Windows hook from spawning PowerShell on every event (#15520)
Antigravity was the last agent posting hook status through Windows PowerShell 5.1. Every
hook event — roughly one every 2-6s during an active session — paid a ~300ms interpreter
cold start, which is what made the console the agent allocates for each hook last long
enough to be seen as continuous flashing.
Move the Windows POST to the shared curl.exe builder every other agent already uses, via
the `extraFormLines` escape hatch for the `hook_event_name` field Antigravity uniquely
needs. Measured on Windows 11: 326ms -> 134ms per event.
Because the curl line percent-expands its arguments, the script also needs
`setlocal DisableDelayedExpansion` (#9358/#9941) so a `!` in a pane key or worktree path
is not eaten as a delayed reference.
curl omits a `--data-urlencode name@-` field entirely when stdin is empty, so accept an
absent or blank Antigravity payload as `{}` at the ingest boundary — the POSIX script
substitutes `{}` before posting and PowerShell did the same, and without this a
payload-less event lost the status transition its `hook_event_name` still carried. Scoped
to that source; every other agent keeps rejecting a body it cannot parse.
Adds a cross-agent guard asserting the invariant the original drift violated: a managed
Windows .cmd hook posts through fully-qualified curl.exe and spawns no interpreter.
Generated under a mocked win32 platform so the POSIX CI legs guard it too.
Validated on a real Windows 11 host, not an emulated platform check.
Fixes #15117
|
||
|
|
cb95582cea | feat(release): build unsigned Windows artifacts for the dev channels (#15465) | ||
|
|
0e5b348414 | fix(repos): keep the desktop-owned manual project order authoritative across paired clients and SSH catalog publishes (STA-4850) (#15538) | ||
|
|
9d1dfc314f |
fix(cli): resolve host names across both kinds, and stop ssh: answering empty (#15449)
* fix(cli): resolve host names across both kinds, and stop ssh: answering empty `--host ssh:<id>` was never validated. An unknown target filtered to nothing and returned ok:true with an empty list — the same silent wrong-machine answer that unknown `runtime:` ids gave before they were rejected. And because SSH target ids are machine-generated (`ssh-<timestamp>-<random>`) while the name anyone actually knows is the label, this fired on the ordinary spelling rather than a rare typo: every human-typed SSH name missed. The two kinds of remote machine are also reached on different axes. A paired Orca server is a connection (`--environment <name>`); an SSH target is a machine the connected host reaches (`--host ssh:<id>`). A caller only knows "the machine called X", so naming X on the wrong axis was the common failure and produced either an empty answer or a dead-end "unknown environment". Now: `ssh:` resolves labels as well as ids and rejects an unknown target with the known ones listed; `runtime:` accepts the environment name as well as its id, matching --environment, and canonicalizes to the id so stored host ids still compare; and when a name misses on one axis but exists on the other, the error says which and gives the exact flag. Candidates ride along in error.data so an agent can recover without parsing prose. `orca host list` is the discovery surface that was missing entirely — nothing in the CLI listed SSH targets, so a caller told to use one had nowhere to look. It prints this machine, the SSH targets registered on the connected host, and the paired servers, each with the selector to use. * fix(cli): give --environment the same cross-kind hint, and validate the ssh host on setup-create Two gaps a follow-up survey found in the first pass. `--environment openclaw` still dead-ended with a bare "Unknown environment" while an SSH target by that name sat right there — the inverse of the case just fixed, and the direction the report actually hit. The store's own error cannot carry the hint: translateStoreError forwards code and message and drops data. So the selector is resolved before the client is built, where the payload survives. Only the explicit flag is asserted eagerly; an ambient ORCA_ENVIRONMENT stays lazy, because failing local-only commands over stale background config would be a regression. `project setup-create` records independent metadata and, unlike the other setup paths, is not covered by the runtime's ssh rejection — so an unknown target persisted a row pointing at a machine that does not exist. It now resolves the host. `local` and `runtime:` still pass through untouched: this is also the provisioning path, where a runtime host legitimately may not exist yet when its metadata is written. `setup-existing-folder` and `setup-clone` deliberately keep the unresolved id. The runtime rejects every ssh host for those operations regardless of whether it exists, so resolving first would answer "no such target" and imply the command would have worked with the right id. * fix(cli): refuse an ambiguous host name instead of resolving the first match Name lookup took the first match while the environment store itself refuses an ambiguous name rather than guessing. That put the guess back, in the selector whose entire purpose is to stop a command reaching a machine the caller did not choose — and it applied to both spellings: two SSH targets sharing a label, and two paired servers sharing a name. Both now resolve to nothing and report every candidate with its id, so the caller picks. An exact id still resolves past a colliding name, since an id is never ambiguous. Also pins the property that makes accepting a name safe at all: `runtime:<id>` is a persisted token that lands in ProjectHostSetup.hostId and is embedded in generated setup ids, so the name is canonicalized to the id before anything downstream sees it. A test now asserts a name never reaches the wire. * fix(cli): fall back to the older ssh listing so an old host is not read as having no targets Hosts predating ssh.listTargetSummaries still answer ssh.listTargets, and both are served by the same summariser. Swallowing the method_not_found made such a host indistinguishable from one with no SSH targets registered, which would reject a target id that is valid there — a new-client/old-host regression on a path that previously passed the id through unvalidated. |
||
|
|
516d91e6bc | Update pull_request_template.md | ||
|
|
fab6e0d6e7 |
fix(mobile): scope optimistic workspace removal to the deleted host (#15424)
* fix(mobile): scope optimistic workspace removal to the deleted host A worktreeId repeats across hosts, so filtering the list on the bare id also removed the identically-named workspace belonging to the other host. Match on (worktreeId, hostId) through a named helper so the rule is testable. * fix(mobile): key host worktree rows consistently |
||
|
|
a61b39a9a6 |
fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) (#15376)
* fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) Two independent frame-of-reference bugs, both from code describing one machine while labelled as another. #15366 — projectHostSetup.* persisted the caller's host id verbatim. Those `runtime:<environment-id>` ids are minted by the calling client's own pairing store, so they name a machine only relative to that client. A client sending one is addressing this runtime, and runtimes do not proxy these calls onward, so the host it names is us. Storing the client's spelling made one machine look like a different host to every other client, hid its rows from them, and defeated the (projectId, hostId) duplicate check — two laptops paired to one server each created their own setup for the same checkout. Re-spell it as `local` at the RPC boundary. Rows written earlier keep their old stamp; readers already project `local` back to `runtime:<their-id>`, so the client-visible model is unchanged and no ids are rewritten. STA-4792 defect 4 — `status --environment <name>` hardcoded app.running:false to mean "no desktop on THIS machine" while every other field in the same object described the target, including a desktopWindowStatus echoed straight from it. The result contradicted itself and read as "that run was headless" when the remote GUI was up. `app` now describes the target, keyed off the one window status that requires a live renderer, and the result names its own subject so the frame can't be misread again. The remote pid is not knowable, so it stays null. STA-4792 defect 2 gets a regression test rather than a fix: routing already made the client remote, which is what stops a Windows destination being joined to the local cwd. The test pins the exact reported invocation. * fix(status): share the remote app projection with the SSH host passthrough, and name the version gap on project host setup Two review follow-ups. The SSH host passthrough answered `app.running: true` unconditionally for the Orca host a caller reached over SSH, claiming a desktop app even for a headless `serve`. That is the same defect as the paired-server path, one transport over, so the projection moved to shared and both now answer the question the same way. `--host runtime:<id>` routes project commands to a paired server, which means a client can reach a server that predates project host setup without meaning to. That answered a raw `method_not_found`, which reads as an Orca bug rather than a version gap; the CLI now names it the way the desktop already does. Reverted a third change: making the persistence duplicate check treat `local` and `runtime:*` as one machine. That assumption holds at the RPC boundary, where a `runtime:` host means the runtime being addressed, but not in the store, which also records independent provisioning metadata for machines that are not itself. An existing test covers exactly that, and it was right. The duplicate convergence therefore stays bounded to rows written after the normalization. |
||
|
|
7fa23438f4 |
fix(mobile): keep a desktop take-back from being undone by a passive viewport report (#15539)
A phone that stays subscribed after 'Take back this/all terminals' re-phone-fits the PTY and re-takes the presence lock on its next terminal.updateViewport, which iOS forces on app resume and on every reconnect. updateMobileViewport consulted only mobileDisplayModes, which reclaimTerminalForDesktop resets to 'auto' before returning, so the take-back had no suppression left. Treat an in-force desktop take-back the same as the existing desktop display mode: record the viewport, apply nothing. |
||
|
|
6415511a82 |
feat(remote): add a positional file read to the filesystem provider (#15517)
* feat(remote): add a positional file read to the filesystem provider Following a growing remote file means re-reading it from the top on every poll: the relay exposes only whole-file reads, so tailing an append-only log over SSH costs O(size) per tick. Adds fs.readFileRange plus a rangedReadVersion capability, and an optional readFileRange on IFilesystemProvider -- matching how lstat/ supportsQuickOpenSearch already declare degradable capabilities. Three deliberate choices: - The relay loops until the requested length is satisfied or the file truly ends, and REJECTS an over-cap request rather than clamping it. A clamped read is indistinguishable from EOF, so a caller advancing a cursor by bytesRead would silently skip data. - Bytes cross the wire base64-encoded. A range boundary can split a UTF-8 sequence at either edge, and a utf-8 round trip would substitute U+FFFD and shift every subsequent offset. - The provider throws a typed FileRangeReadUnsupportedError against an older relay instead of quietly falling back to a whole-file read. A tailing caller issues several reads per snapshot, so a per-call fallback is quadratic; callers probe supportsFileRangeRead once and snapshot instead. The response is validated before use -- a byte count disagreeing with the payload would shift every downstream offset while looking like success. Terminal-artifact reads/writes move to their own module, mirroring the relay's existing fs-handler-terminal-artifact split; the provider was at the max-lines ceiling and this was the cohesive piece to extract. * fix(remote): size the ranged read to what the relay writer can deliver The 4 MiB cap was justified against MAX_MESSAGE_SIZE (16 MiB), but that is the frame DECODER bound. Responses are gated by the writer's admission budget: a frame over DISPATCHER_CONTROL_QUEUE_MAX_BYTES (1 MiB) is demoted to the legacy-response lane, which is refused once the producer queue passes 2 MiB. A 4 MiB window is ~5.46 MiB of base64, so it was never admissible -- it came back as an opaque ResponseOverCapacity (-33008), which is neither of the PR's typed errors, and above ~1.4 MiB the outcome depended on unrelated queued traffic. Cap at STREAM_CHUNK_SIZE (256 KiB), the house per-frame budget for file bytes, which stays in the control lane unconditionally. Also: - Hoist the cap and offset validation into src/shared/file-range-read.ts so the client rejects an out-of-contract request locally instead of paying a round trip for an error that does not survive the wire as a type. - Validate filePath in the relay handler; a missing one threw a TypeError out of expandTilde despite the comment claiming hand-validated params. - Collapse the two fs.getCapabilities probes onto one cached fetch per multiplexer. They read one document, so probing per feature spent an extra round trip per connection and duplicated the eviction logic. - Reuse readFullStreamChunk instead of a second copy of the short-read fill. - allocUnsafe the window; only subarray(0, bytesRead) escapes, so a tailing poll no longer memsets the whole window per call. - Plain methods for readFileRange/supportsFileRangeRead rather than constructor-assigned arrows; both are unconditional, unlike downloadFolder. Tests: cover the dispatch path and fs.getCapabilities (neither was exercised), param validation at both boundaries, EOF at and past the end, and a full-cap read over a real RelayDispatcher. The transport guard fails at 4 MiB with the real -33008. * test(remote): pin the ranged-read cap to real control-queue headroom The cap comment claimed a full-cap window stays in the control lane "unconditionally" and the guard test only asserted one frame fits the lane, so a raise to 384-768 KiB stayed green while two concurrent full-cap responses would already overflow the shared control queue -- which for a response closes the client. Pin the two-deep headroom and state the real bound, including that widening the cap is a wire change against a host still advertising rangedReadVersion 1. Also cover the two behaviours the suite claimed but did not exercise: a regular file answers a full-cap read in one syscall, so the fill loop was untested (both mutations of readFullStreamChunk stayed green), and the merged capability document made the abort-does-not-evict guard load-bearing without any test reaching it. * fix(remote): harden ranged-read validation and retry |
||
|
|
d4a8da9fdc |
fix(browser): scope a native cookie import's clear to the domains it imports (STA-4797) (#15375)
* fix(browser): scope a native cookie import's clear to the domains it imports (STA-4797) A native import (Settings -> Browser -> Import Cookies -> From Google Chrome) cleared the entire target partition's cookie jar, keeping only the non-transplantable google.com family. Every unrelated site the user was signed into in that partition was silently signed out, with no warning before and no disclosure after. Importing three sites signed you out of every other one. The stated rationale -- mixing stale and imported cookies makes sites reject the session -- reaches only as far as the domains being imported. Beyond them a clear has nothing to reconcile. The file/paste path already did the narrow thing via replaceCookiesForImportedDomains; the two import paths simply disagreed about scope, and the narrow one is the defensible shape. The clear now covers only the domains the import writes, through one shared predicate: - browser-cookie-import-policy.ts: importedDomainScope() / domainIsInImportedScope() are exported as the single scope definition used by all three clears, so they cannot drift apart. - browser-cookie-import-clear.ts: removeTransplantableCookies takes a required importScope. It is not defaulted -- a default would be the whole jar again. The scope test runs before the removal-URL derivation, so an unaddressable cookie parked in an unrelated corner of the jar no longer fails an import that was never going to touch it. - The bulk clearData shortcut is gone, and clearData leaves CookieClearSession for the same structural reason 'set' already had. clearData clears by exclusion, so the only scope it can express is "everything except google.com" -- the defect. An include list is no better: it matches at the registrable-domain boundary, so it would still take host-only siblings the import does not replace, and a partial delete followed by a rejection would destroy them with no identity to restore from. The frozen per-coordinate plan is now the only removal path, and it covers the imported domains rather than the jar. - browser-cookie-staged-image-clear.ts (new): the staged image is a copy of the live jar that replaces it wholesale on the next cold start, so its DELETE FROM cookies WHERE NOT (<google>) was a second whole-partition wipe. Narrowing the live clear alone would have re-erased the partition one restart later. It now clears to the identical scope, through the identical predicate. The scope is named from the emitted plan, so the removal set is the write set. Google stays exempt by policy (STA-3811), unchanged. No wire or summary change: the summary's existing `domains` field already names the imported domains, which is now exactly the scope that was cleared. Tests: fixtures in this module start with an empty cookie jar, which is why a full gate stack passed a session-erasing defect before. browser-cookie-import- scope.test.ts uses a populated jar with a session for a site outside the import set, and reads the staged file itself so the restart path is observed rather than assumed. All three cases fail against pre-fix source. The real-Electron partition test now seeds both an in-scope stale cookie (still removed) and an out-of-scope live one (now survives). * refactor(browser): drop the unreachable removal-URL failure branch (STA-4797) Scoping the clear made the `Could not clear existing cookies` throw in removableCookieEntries dead: a cookie whose domain does not normalize is now skipped by the scope test above it, so nothing reaches the URL derivation with anything but an already-parsed hostname. Rather than leave a fail-closed branch that cannot fire — this module has been misread before when a dead safety leg looked like a live one — the impossibility is now structural. cookieRemovalUrl takes a normalizeCookieDomain output and returns a string: `new URL` cannot throw on a host that already parsed as one, and assigning pathname never throws. Both callers lose their null branch, including the silent `if (url)` skip in replaceCookiesForImportedDomains, which would have narrowed a removal plan without saying so. The identically-worded throw in assertClearIdentitiesCoverRemovable is untouched — that one is live and is what keeps the mutated set inside the restorable set. * test(browser): re-anchor the native concurrency detector on the scoped clear (STA-4797) #15095's detector read "has the second import started clearing yet?" off clearData call counts. Scoping the clear removed the bulk clearData path, so that signal is gone and the assertion measured nothing. It now reads the same question off the removals themselves, which is strictly more specific: the seeded jar holds a stale cookie for each import's own domain, so `remove:old-a` present with `remove:old-b` absent proves the first import cleared and the second has not — where a call count could not tell the two apart. The completed run then pins the exact removal sequence, which also records that each import clears only its own domain. The seed had to move onto the imported domains for the same reason its own comment already gave for not leaving the jar empty: under a scoped clear, a jar holding only an unrelated site is the empty-jar case wearing a disguise -- the clear returns having removed nothing and every assertion passes vacuously. Mutation-checked: with the per-partition lock removed this test still fails, so #15095's protection is intact and the re-anchoring did not hollow it out. * fix(browser): merge staged cookie imports by domain scope (STA-4797) |
||
|
|
fcdbcf85d0 | fix(terminal): keep a cold-parked pane's runtime-graph leaf while its PTY lives (STA-2854) (#15514) | ||
|
|
7675da363e | fix(remote): stop paired-tab resurrection, ghost agent rows, and frozen visible panes (STA-4593) (#15459) | ||
|
|
0a853a5c0d |
fix(agent-hooks): stop backgrounded Claude sessions posting a stale pane key (STA-4769) (#15304)
* fix(agent-hooks): stop backgrounded Claude sessions posting a stale pane key (#9236) A session started with `claude --bg` or `/background` runs in a worker under the shared daemon, and that worker inherits the environment of whichever pane first started the daemon — not the pane that dispatched it. ORCA_PANE_KEY there names an unrelated pane, so the session's hooks overwrite that pane's sidebar row, from any worktree. It fails silently and successfully: the script re-sources the endpoint file, so port and token self-heal and the POST lands, while the pane key has no refresh path and stays wrong. Measured on the wire against a throwaway listener: three sessions on one daemon, and the one dispatched from pane B posted pane A's key and pane A's worktree on SessionStart, UserPromptSubmit and Stop. CLAUDE_JOB_DIR is set only in those workers — absent from all 68 live foreground sessions on this machine — so it is the signal to decline. Declining is the only option that exists: normalizeHookPayload rejects an absent paneKey outright and AgentHookEventPayload.paneKey is a required string, so there is no "session with no pane" representation to post instead. A backgrounded session genuinely has no pane; attributing it to nothing is correct. The Windows guard exits rather than jumping to the stdin drain: the drain parks in more.com and a daemon worker is outside an Orca pane, which is exactly the abandoned-stdin hang #11549 guards against. * fix(agent-hooks): guard the Claude statusline against the same stale pane key The statusline command IS invoked inside a backgrounded worker — measured, with no client ever attached, and its ancestry terminates at the daemon rather than at any pane: statusline pid=41746 <- claude bg-spare <- claude bg-pty-host <- claude daemon CLAUDE_JOB_DIR=/tmp/.../jobs/f1f9edd2 A second session dispatched from a different pane saw the first pane's ORCA_PANE_KEY with its own correct session id, so this script is a live second producer of the same misattribution the hook guard closes. Windows uses exit /b 0 before stdin is owned, per the #11549 contract; POSIX places the guard after capture, since exiting mid-write there surfaces as EPIPE the agent can see (#8110). |
||
|
|
15d2e31777 |
ci: bound the shell-contracts apt install so a stalled mirror cannot wedge the run (#15532)
* ci: bound the shell-contracts apt install so a stalled mirror cannot wedge the run shell contracts wedges intermittently. It is not a lock, not a prompt, and not the PR under test - it is download throughput with no wall-clock bound. Measured on a passing run: apt-get update fetched 11.4 MB of index in 40s, then apt-get install fetched 8.9 MB of packages at 65 kB/s taking 2m17s, while the shell-contract tests the job exists to run took 14s. apt applies no wall-clock bound to a stalled mirror, so when throughput drops below that already-poor baseline the step runs indefinitely - observed at 12+ minutes and climbing while every other job had passed. The job also had no timeout-minutes, so it inherited GitHub's 6h default, and an in-progress required check holds the whole run open and blocks rerun --failed. Bounds both layers: timeout-minutes on the job (a passing run is ~4m30s), and Acquire timeouts plus retries in apt.conf.d so a dead mirror fails fast while a transient blip still passes. Written to apt.conf.d rather than onto the command lines because pr-workflow-parallelism.test.mjs parses those invocations. Does not make the job faster; the 3m38s of download is untouched. Scoping the index refresh to just the PPA risks installing against a stale base index and wants its own evidence. * ci: bound the apt commands by wall clock, not per-connection timeouts The first attempt at this set Acquire timeouts of 30s with 3 retries. That made the wedge worse and the job's own timeout-minutes proved it: on this PR the install step ran 14m26s and was killed by the 15 minute bound. The log shows why. Acquire timeouts are per-connection, so a dead mirror costs timeout x retries x every index file: 30s x 3 across roughly ten index files is ~15 minutes, which is what was observed. The azure archive mirror returned Ign for every suite, apt fell back to archive.ubuntu.com, and that connection then produced zero bytes for 14m26s. So per-connection bounds cannot bound this step; only a wall-clock bound can. Wraps both apt invocations in `timeout`, and drops Acquire::Retries to 1 so a dead mirror fails once instead of multiplying. The update is already tolerant by design, so bounding it just caps what a dead mirror costs before the install runs against whatever index exists. Also drops DPkg::Lock::Timeout: no lock contention was ever observed in these logs, and an option added on speculation is not worth carrying. |
||
|
|
174039a14e | fix(workspaces): re-seed a terminal when an emptied workspace is opened (#15513) | ||
|
|
9b8dbd8930 |
fix(mobile): keep a terminal lease alive when the handle lookup fails (#15463)
* fix(mobile): keep a terminal lease alive when the handle lookup fails `waitForTerminal(..., 'exit')` rejects with `terminal_handle_stale` whenever the handle cannot be resolved right now. The loudest source is `record.rendererGraphEpoch !== this.rendererGraphEpoch` — every renderer graph reload invalidates handles issued before it, on panes whose PTY is untouched. All three terminal.subscribe branches wired that rejection straight to teardown (`.catch(() => registration.releaseIfCurrent())`), so the host retired a live lease and emitted `end`. Mobile reads `end` as "PTY gone" and rearms; after MAX_REARM_ATTEMPTS = 3 it stops and leaves the composer on "Waiting for terminal…", recovering only when a replacement handle arrives. A live PTY keeps its handle, so no replacement ever comes: the pane is dead until the app is backgrounded and reopened. Demand proof instead. Retire on abort (this socket is going away) or when isLeafPtyProvenAbsent says the process is gone; a real exit still resolves and retires through the .then leg. Absence must be proven, never inferred from a lookup that failed. Reproduced live: a paired 0.0.44 client over LAN, fault injected on the handle lookup, composer locked on "Waiting for terminal…" and STILL locked 97s after the fault was lifted with the PTY alive and the handle unchanged. Narrows one behavior #14992 pinned: a stale handle alone no longer retires the owner. The ownership property is unchanged — once absence is proven, the owning registration is still what retires. That test is updated, not deleted. * fix(mobile): observe PTY exit independent of stale handles * fix(mobile): stop subscribe setup when an exited pty releases synchronously The runtime doubles for terminal.subscribe never stubbed subscribeToPtyExit, so the new lifetime watcher threw and killed every subscription under test. Also return early when the watcher releases synchronously: cleanup had already run, so the stream handlers, view subscribers, and mobile presence registered afterwards were never torn down. |
||
|
|
a3b3254885 |
fix(browser): grant storage-access so requestStorageAccess() stops rejecting (#15481)
* fix(browser): grant storage-access so cross-site frames can use their cookies AUTO_GRANTED_BROWSER_PERMISSIONS omitted storage-access, and the installed permission handlers deny anything absent from that set, so every document.requestStorageAccess() in the embedded browser was refused silently. Granting it unconditionally looks like a privacy hole, but that objection assumes Orca blocks third-party cookies. It does not: nothing blocks or partitions them and no Chromium switch touches cookie policy, so Electron's default applies. A cross-site frame therefore already reads and writes its unpartitioned cookies at the network layer, and denying the permission grants no protection - it only breaks sites that take the API's failure path. Chrome resolves requestStorageAccess() without a prompt under the same cookie policy; Electron has no such fast path and forwards to the embedder, so the embedder supplies it. The comment records the condition that would invalidate this. top-level-storage-access stays denied. requestStorageAccessFor() is a separate platform decision: Chromium consults Related Website Sets and has no third-party-cookie auto-grant, and Orca has no such data source, so granting it would invent a permissive answer to a restrictive question. A test pins it. Does not fix Google sign-in - that was #15216. Sign-in completes with this denial in place; this is an independent defect found while investigating it. * test(browser): pin the denial notice storage-access used to raise The suite proved the handler answers true but never pinned the symptom users actually reported — the "asked for storage-access, and Orca denied it" notice. The existing notified-list assertion runs before the storage-access request, so a regression that re-denied it would have left that list untouched. Assert the list after the new requests instead; it goes red pre-fix with 'storage-access' present. Drop the two vi.waitFor wrappers around the same requests. The request handler is synchronous for every non-media permission, so the wait bought nothing and imposed vi.waitFor's 1000ms default on a suite that allows 30s. The waitFor guarding the media path is genuinely async and stays. * refactor(browser): tighten the storage-access rationale and cover isolated partitions Trim the grant's comment to the facts that change a reader's decision. The mechanics of how the request reaches the embedder already live in the commit message; what belongs at the call site is why the answer is grant, and why the check handler must agree with the request handler — Electron builds neither Chrome's activation gate nor its auto-grant, so a disagreeing check pushes compliant sites onto the gesture path, where a rejection consumes the gesture. Move the top-level-storage-access note below the set. It sat after the last element with no trailing comma, so it read as a commented-out entry, and any permission appended at the natural insertion point landed above it. Cover the isolated-partition install path. createProfile and hydrateFromPersisted call installBrowserSessionPartitionPolicies separately from the default-partition path the persistence suite drives, and the two previous changes to this set each added a matching isolated test. Verified red without the grant. Rename the anti-detection case that claimed storage-access has a native denied state; it is granted in production now, and the case really pins pass-through. * docs(browser): correct the storage-access rationale The revisit trigger was backwards. If Orca ever blocked third-party cookies the grant would not become dangerous, it would become useless for cookies: Electron builds no HostContentSettingsMap, so no STORAGE_ACCESS content setting is ever written, and IsAllowedByStorageAccessGrant needs one. Point the tripwire at a cookie or storage-partitioning control instead, which is the change that would actually invalidate the reasoning. The premise was also narrower than the grant. Third-party storage partitioning is enabled by default and independent of cookie policy, and the same permission lifts it for localStorage, IndexedDB, CacheStorage and friends via StorageAccessHandle, which gates only on IsFullCookieAccessAllowed. So the frame does not "already have" everything this grants. It stays the right answer because Chrome grants the same permission under the same cookie policy, but the comment should not claim a narrower blast radius than the change has. * docs(browser): give the storage-access tripwire its consequence Say what goes wrong, not just when to look. If Orca ever blocks third-party cookies or gains a partitioning control, three separate gates stay shut in Electron - the network-service grant check, the frame's trusted status, and the STORAGE_ACCESS content setting that is never written - so the promise would resolve while access stayed blocked. Sites follow the documented pattern of reloading after a successful request, and on reload the check handler still reports granted, so no gesture is needed to ask again. That loops. Qualify the non-cookie clause: the no-arg call resolves undefined and touches only cookies. It is the dictionary form that returns a handle, and since the handler sees the permission name and never the call shape, one grant covers both. Also give "check must agree with request" its reason. Justify the isolated-partition test by the precedent it follows rather than by a call-site divergence the shared mock cannot actually distinguish. * docs(browser): scope the non-cookie clause to the handle A live probe pinned down what the grant actually widens. The frame's ambient window.localStorage and window.indexedDB stay partitioned before and after a successful request; the unpartitioned view is reachable only through the handle the dictionary form returns. Existing code in the frame is unaffected unless the site explicitly calls through that handle, so say handle-scoped rather than leaving a reader to assume the globals change. * docs(browser): correct the isolated-test justification and the tripwire scope "the isolated twin every other entry in this set already has" is false. Counting occurrences in browser-session-registry.test.ts: fullscreen, clipboard-read and clipboard-sanitized-write have none. Cite the pointerLock precedent the test actually mirrors, which is the case directly above it. Separate the two gates in the tripwire. The STORAGE_ACCESS content setting gates cookies; the handle path is gated on IsFullCookieAccessAllowed instead, and a live probe confirmed it works today. Saying "access stayed blocked" read as a claim that the handle is backed by nothing, which contradicted the sentence above it. Say cookie access, and say the handle survives. |
||
|
|
0b8107a41f |
fix(browser): stop the auth-host UA write from cancelling redirects (#15221)
* fix(browser): stop the auth-host UA write from cancelling redirects WebContents.setUserAgent() from will-redirect makes Chromium abort the in-flight navigation (ERR_ABORTED) and replay the original request, so any redirect crossing the Google auth-host boundary dies on a blank tab. Route that write through the CDP override, which retargets navigator.userAgent without touching the navigation. Fixes #15216 * fix(browser): recover auth UA override state * fix(browser): keep the viewport UA override on the session identity The auth-host switch writes the Firefox UA through WebContents.setUserAgent on a direct navigation and then moves to the CDP override, and nothing restores that WebContents UA. sendViewportUserAgentOverride read it back as its base identity, so a tab with a viewport preset republished the Firefox UA on every ordinary host afterwards, with sec-ch-ua still saying Chrome. Read the profile's session UA instead, which is the stable base identity at both call sites. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
73f7767edd |
feat(sidebar): name the host when a delete batch collides on one id (#15423)
* feat(sidebar): name the host when a delete batch collides on one id Two hosts can publish the same worktreeId, so a batch confirmation showed two rows with identical names and paths and nothing to tell them apart. Label each row with its execution host, but only when the batch actually contains a same-id collision — an unconditional chip is noise. * fix(sidebar): qualify colliding delete targets by saved host * fix(sidebar): preserve delete host collision scope |
||
|
|
4fc8b65792 |
docs: add WeChat group 8 as fallback when group 7 is full (#15466)
Provides alternative community group with QR code when primary group reaches capacity. Updates documentation in both English and Chinese. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
391385fd5c |
fix(workspace-cleanup): require a host-qualified key for every candidate row (#15422)
The virtualizer keyed rows by bare worktreeId, so two hosts sharing an id collapsed onto one key in the confirm-remove dialog. getRowKey is now a required prop supplied host-qualified by each caller, making omission a compile error rather than a silent fall back to the colliding id. |
||
|
|
41c4d8e8a8 | Update README downloads badge | ||
|
|
90a292eec2 |
Fix WSL stall tests to reset gate state and fake performance timer (#15414)
Reset persistent WSL transcript filesystem gate state in beforeEach to prevent prior test stalls from quarantining subsequent tests. Fake the performance timer used by the route quarantine clock so tests don't block on real time. Update affected tests to wait out the back-off window rather than advancing by zero time. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
1ca752a7a4 |
test(e2e): keep the native Hangul reproduction harness (#15438)
* test(e2e): keep the native Hangul reproduction harness This is the spec that reproduced #15299: it drives a real ibus-hangul engine through a real compositor and asserts the bytes reaching the pty. It is the first setup here that can exercise an input method end to end, and three IME issues this week were unreproducible without one. It does not run in CI, and the header says so rather than implying coverage. It needs a compositor session CI does not have, and this repo already carries native IME specs that are skipped everywhere and were mistaken for protection they never gave. The run recipe is in the header so the next person does not rebuild it. Recorded there too are the five things that decide whether a run is real or a silent false negative - nested rather than headless, an unused display, a session script that does not exit, forcing the window visible, and sending Escape before the byte reader starts. Each cost a failed attempt, and four of them are what defeated an earlier try. Keys and expected text are environment-tunable so other IME issues can reuse it unchanged. Refs #15299 * test(e2e): record three more silent-false-negative traps in the native IME harness A Hanja candidate-selection run on the same rig hit all three. Each produced an empty or misleading event log that reads as "the IME ignored the key" rather than as a broken harness, which is the failure mode this header exists to prevent. The panel one is the least obvious: a session whose ibus-daemon runs with --panel=disable never draws a lookup table, so any run that depends on seeing candidates measures nothing while appearing to work. Refs #15299 |