mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
2efc6e5476f2b02bfe2005a1d8d6f9ce50fcb891
6017
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0c2893695c |
fix(browser): keep browser tabs rendering across worktree switches (STA-3228) (#12137)
* fix(browser): keep browser tabs rendering across worktree switches (STA-3228) Switching away from a worktree that had a targeted background mount unmounted BrowserPaneOverlayLayer, pulling the persistent <webview> slots out of the DOM and killing their guests; the stale viewport cache then kept rendering into the removed subtree, so the tab stayed blank forever and reload threw. Keep the overlay mounted for hidden worktrees (slots park their panes, so this stays cheap) and rebuild cached viewports whose slot root remounted. * fix(browser): retain live overlay slots without background churn * fix(browser): latch overlay retention after commit |
||
|
|
99b94a38eb |
docs(tasks): trim the search-window error pattern comment (#12130)
Keeps only the non-obvious rationale for pinning GitHub's free-text 422 wording. Co-authored-by: Orca <help@stably.ai> |
||
|
|
56ab5fd1dc |
fix(tasks): make GitHub pagination honest — cap unreachable pages, survive background refreshes, explain empty pages (#11584)
* fix(tasks): cap advertised GitHub pages at the search result window GitHub's Search API rejects requests past its first-1000-results window with HTTP 422, but totalPages was derived from the raw total_count, so the pagination bar advertised pages that could never load and clicks on them silently did nothing (#11485). Cap per-repo advertised pages at floor(1000 / perRepoLimit), and when a page load comes back empty, say so with a toast instead of ignoring the click — clamping the advertised count only when no fetch threw, so transient failures don't shrink the bar. * fix(tasks): key pagination resets on repo selection, not array identity The repos store installs a fresh array on every repos:changed event, so the pagination-reset effect fired on background refreshes and bumped the request generation, silently discarding any in-flight page navigation — clicking an unloaded page did nothing whenever a repo refresh landed during the fetch. Key the effect on the stable selection string instead. * fix(tasks): distinguish end-of-data, window 422s, and failures on empty pages Adversarial-review round 1 rework: - fetchWorkItemsNextPage now returns issue-side envelope error types — the channel the search-window 422 actually travels on (failedCount only counts thrown repo calls). - resolveEmptyPageOutcome (unit-tested) maps an empty page to window-unreachable (clamp + toast), load-failed (toast only; may be transient), or end-of-data (silently withdraw the speculative page the count-fallback advertises). - The work-items fetch effect is keyed on selectedReposKey too — its unconditional page reset re-fired on every repos:changed array identity, bouncing the user to page 1 mid-click. The key now includes the resolved GitHub source context so identity changes still re-dispatch. - Toasts carry stable ids so repeats replace instead of stack. - Cap comment documents the conservative PR-scope tail loss; cap tests pinned at shipped (36 → 27) and dividing (25 → 40) limits. * fix(tasks): withdraw the speculative page when the failed count is zero countedTotalPages of 0 comes from a swallowed count failure and routes totalPages through the fallback, so the clamp must replace it like null. * fix(tasks): tighten empty-page outcomes after round-2 review - en.json's loadPageUnreachable carried the pre-reword text, and the catalog beats the inline default — the two toasts were identical. - end-of-data clamps only while the count is unknown/failed: the PR list path swallows its own failures into clean-empty results, and clamping a real count silently hid healthy pages (worse than the pre-fix no-op). - A window 422 no longer clamps when a sibling repo's fetch threw. - The generation effect mirrors every fetch-effect dep that resets page state, so manual refresh/source switches invalidate in-flight clicks. - selectedReposKey extracted as buildSelectedReposKey with stability tests; envelope error types wire-tested through the store. * fix(tasks): clamp against the committed count, not the click-time closure Round-3 review: the count promise routinely resolves between click and response, so deciding the end-of-data clamp from the closure value let a stale null overwrite a real count. applyEmptyPageClamp now runs inside the functional updater against the committed value, never raises an earlier clamp, and a window 422 coinciding with a thrown sibling repo resolves as load-failed so the toast and the clamp always agree. * fix(tasks): only an all-window-422 empty page may clamp; harden count merges Round-4 review: a sibling repo's envelope 403/404 arrives with failedCount still 0, so the window branch now requires every error to be the window 422 (non-window validation errors are demoted at the store); the count resolution mins against an applied clamp instead of re-advertising withdrawn pages; the generation effect mirrors taskResumeApplied so its doc claim holds. * fix(tasks): split the proven window limit from the count slot Round-5 review: min-ing the count against an applied clamp pinned a SPECULATIVE end-of-data withdrawal that raced ahead of the count, permanently collapsing the bar for the generation. Proven window-422 limits now live in provenPageLimit (set once, only lowered, reset per generation); the count overwrites its own slot unconditionally; and deriveAdvertisedTotalPages (unit-tested for both arrival orders) caps the count-or-fallback estimate with the proven limit, floored at the loaded pages. * fix(tasks): surface PR-side list failures so they can't read as end-of-data Round-6 review: PartialWorkItemsResult had no PR error slot, so a swallowed gh pr list failure reached the renderer as a clean empty page — and with the count blocked (0) the speculative withdrawal deleted the pagination bar with no toast and no recovery (a regression vs main's silent no-op). PR-side errors now ride the envelope (errors.prs), demoted so they can never join the issue-only window-422 signal; errorTypes replaces issueErrorTypes; an empty page that a real count said should exist now toasts instead of looking dead. * test(tasks): cover the PR-error envelope end-to-end; neutral no-more-results toast Round-7 review: the two literal gh-utils mocks lacked classifyListPrsError (a PR-side rejection in those suites would TypeError instead of assert), and the producer half of the errors.prs contract had no main-side test — added both, plus a classifier contract test pinning the search-window phrase the renderer keys on. The refused-clamp toast now reads the committed count via a synchronous ref mirror instead of the click-time closure, and says 'No more results' — nothing failed on that branch. Both toast keys plus the new one are translated in es/ja/ko/zh. * fix(tasks): preserve final reachable GitHub search page * Extract GitHub search result window error pattern to shared constant Extract the 1000-result window detection pattern to a single source of truth so the classifier and consumer stay synchronized. The pattern is the only signal separating a permanently unreachable page from a transient validation failure, so drift or trimming silently demotes window 422s to generic failures and stops capping the advertised page count (#11485). --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
98ae8e4c8c |
Allow clearing all agents from AI Vault session history filter (#12128)
* Allow clearing all agents from AI Vault session history filter Add "Select all" / "Clear" buttons so users can quickly isolate one agent without unchecking each box individually. Previously, at least one agent had to remain enabled; now users can filter to zero agents and re-enable selectively. * Address PR #12128 review feedback - Make Select all / Clear real DropdownMenuItems so Radix roving focus reaches them by keyboard. - Rename the zero-agent empty state to a neutral "No agents selected" now that zero agents is a valid filter. - Use 모두 해제 for the Korean Clear label instead of 지우기 (erase). Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
0ae9174408 |
refactor(github): extract shared GitHubItemDialog/PullRequestPage code (#12092)
Both hosts carried token-identical copies of the GitHub work-item mutation wrappers, PR diff mapping, presentation formatters, and four components. Move them into src/renderer/src/components/github/ so there is one source. Behavior-neutral: getStateTone, WorkItemStateBadge, PRReviewersPanel, PRActionsPanel, CommentReplyForm, and the per-host work-item/PR-file caches stay in place because they genuinely differ between the two hosts. CommentCodeContext takes loadPRFileContents as a prop so each host keeps its own private file-contents cache. Also folds github-issue-comment-helpers.ts into github-user-avatar.tsx and retargets the textual boundary tests at the new modules. |
||
|
|
07bd574294 |
refactor(usage): share the Codex/OpenCode scan fold behind a provider contract (#12082)
* refactor(usage): share the session/daily fold between Codex and OpenCode The Codex and OpenCode scanners each carried their own byte-identical copy of the ~325-line aggregation pipeline (createEmptySession, the three breakdown folds, finalizeSessions, mergeSessions, mergeDailyAggregates). Two copies means a token-accounting fix — a bucket that double-counts, a merge that drops a breakdown row — lands in one provider and silently not the other. The copies had already started to drift in comments only; the next drift would have been in arithmetic. The providers differ in exactly one dimension: the extra metric folded alongside the token counters (Codex `hasInferredPricing`, OpenCode `estimatedCostUsd`). That is now injected as an empty/fromEvent/fold triple, so the shared code stays generic without collapsing the two record schemas into a nullable union. The clone strategy stays per-provider (`cloneSessionForMerge` vs `structuredClone`) rather than being unified on the assumption that the difference is accidental. `usage-provider-contract.ts` is the seam a plugin-contributed usage source will implement. It is deliberately generic over each provider's record types: Claude bills per turn while Codex/OpenCode bill per event, and `cachedInput` is a subset of `input` for the latter but a peer bucket for Claude, so a single normalized record would push nullable handling onto every consumer. No behavior change. Emitted objects are byte-identical, including key insertion order — verified by diffing JSON.stringify of the scan output before and after across mixed models, mixed locations, an inferred-pricing flip, and null vs non-null cost. Persisted field names and schemaVersion are untouched, so caches do not invalidate. * refactor(usage): make the provider contract load-bearing and dedupe worktree refs Follow-up to the aggregation extraction, addressing three review points. `UsageProvider`/`UsageScanResult` were declaration-only, which is the same speculative-interface problem #12077 just deleted 8,900 lines of. They are now implemented by both real providers via `satisfies`, so the seam is typechecked against actual scan functions rather than asserted. The blocker was that codex returns `processedFiles` and opencode returns `processedDatabases`; rather than rename persisted-adjacent fields, the source key is a type parameter, so each provider keeps its own on-disk name and the contract still binds. Verified the constraint bites: swapping the key to 'processedSources' fails typecheck. `schemaVersion` is part of provider identity in the contract, so each provider's SCHEMA_VERSION constant (with its cache-invalidation rationale) moves into the provider module and the store imports it. Values are unchanged (codex 5, opencode 2) and the stores compare them exactly as before, so no cache invalidates. This also keeps store -> provider -> scanner acyclic. `UsageWorktreeRef` collided with the existing export in usage-worktree-metadata (3 fields, no repoId). Two different exported types under one name in src/main is worse than the duplication being removed, so the scan-input type is now `UsageScanWorktreeRef`; usage-worktree-metadata is untouched. `createWorktreeRefs` was triplicated. Codex, OpenCode, and Claude copies are byte-identical apart from the return type name (verified by diff), and all three ref types have the same four fields, so one shared copy replaces all three. This is the only change to claude-usage/. No behavior change: same functions, same arguments, same call order. The store tests' `./scanner` mock still intercepts scanning because the provider captures the mocked binding; their now-inert `createWorktreeRefs` mock key is dropped so it does not read as still mocking something. |
||
|
|
60e6a192cb |
refactor(worktrees): reuse the shared push-target helpers on the SSH path (#12091)
findRemoteForUrlSsh, ensureUniqueRemoteNameSsh and
configureCreatedWorktreePushTargetSsh were byte-identical to
findRemoteForUrl, ensureUniqueRemoteName and
configureCreatedWorktreePushTargetWithExec in worktree-push-target-setup.ts,
differing only in calling provider.exec instead of the injected execGit.
|
||
|
|
484273844a |
feat(updater): add an adhoc release channel for branch builds (#12051)
* feat(updater): add an adhoc release channel for branch builds Hourly covers main. This covers everything that is not main yet: a dispatchable macOS build of an unlanded branch, published to stablyai/orca-adhoc, so the team can run an experimental feature for a few days instead of reasoning about it from a diff. Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' < 'rc' < stable — so no routine check can walk anyone onto somebody's branch; only an explicit pinned jump reaches one. It gets its own repo rather than sharing orca-hourly's, because a branch build must not appear in the list a developer riding main is looking at. Signed and notarized exactly like hourly, for the same reason: macOS anchors a notarized app's TCC grants on identifier + team, so an unnotarized build reads as a new client and silently loses file access under Documents/Desktop/Downloads. Tags stamp to the second rather than the minute. Hourly runs under a concurrency group and cannot overlap itself; adhoc builds are dispatched on demand, so two people cutting from different branches inside one minute is ordinary — and a minute-resolution tag would collide and fail the second build after its whole pack-and-notarize run. Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS list: repo mapping, macOS-only support, and UpdateSource. The RPC schema that validates releaseChannelOverride was a hand-copied enum missing the new channel, which would have rejected the override on its way to the main process; it reads the predicate now. * fix(updater): merge the duplicated shared/types import Co-authored-by: Orca <help@stably.ai> * fix(ci): default the adhoc build ref to the dispatch branch The Actions UI puts its own "Use workflow from" branch picker directly above the ref field, and picking a branch there is what most people read as "build this". Making the field optional means the obvious action is also the correct one; naming a branch explicitly still wins, so main's copy of the workflow runs rather than a stale one on an old branch. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5390224bf7 |
fix(relay): declare reconnection to the director's verified fast lane (#12086)
Recovery and broker open now send the optional reconnect hint so the director admits already-assigned hosts through its bounded fast lane (orca-cloud#212) instead of the placement queue that starved session recovery during the 2026-08 incident. A rolled-back director that rejects the hinted field gets one unhinted retry. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
3f8654c26e |
fix(editor): make lazy-chunk recovery actually reload instead of being silently vetoed (#11929)
* fix(editor): stop filing crash reports for expected lazy-chunk swaps RichMarkdownErrorBoundary reported every caught error as a react-error-boundary crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws after it has already exhausted its retries and its one guarded reload. That sentinel means "the chunk hash changed under a running window" (an app update), which is deliberate graceful degradation, not a crash. RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt surfaces LazyChunkLoadError and files a report. The fallback UI is unchanged, so the pane stays usable and offers retry. * fix(editor): prove the lazy-chunk reload landed before suppressing crash reports - lazy-with-retry: reload guard stores the requesting document's identity, so a vetoed reload() no longer reads as "recovery ran" (crash b860def2) - lazy-with-retry: bound the post-reload suspension so a vetoed navigation surfaces the real error instead of hanging the pane on a spinner - RichMarkdownErrorBoundary: contain the LazyChunkLoadError sentinel without a crash report, but record a lazy_chunk_boundary_degraded breadcrumb - EditorContent: name the rich markdown chunk at the lazy call site Co-authored-by: Orca <help@stably.ai> * fix(editor): route lazy-chunk recovery reload through the intentional-restart path Crash b860def2's recovery reload was requested and never landed: Terminal's beforeunload handler preventDefault()s while any editor tab is dirty and Electron cancels the navigation with no dialog, so chunk recovery could never run in the common case. Take the updater's path instead — hot-exit backup, one synchronous session checkpoint, restart latch — then reload. - Reject on ORCA_RENDERER_UNLOAD_PREVENTED_EVENT instead of a blind, never-cleared 10s timer; keep the timer only as a backstop. - Record a lazy_chunk_reload_vetoed breadcrumb in the same tick as the report it now files, so the 30-entry ring cannot evict the evidence. - Drop this document's own stale guard after a refused reload (capped in memory) so saving the blocking tab does not forfeit recovery for the session. - Carry reloadKey on LazyChunkLoadError and the degraded breadcrumb. - Move renderer-restart-preparation to src/shared: it is now a renderer/preload contract, and the composite web project cannot import preload runtime code. Co-authored-by: Orca <help@stably.ai> * fix(editor): clean up failed lazy chunk reload requests * test(preload): exercise restart IPC registrations --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
0d38053945 |
fix(i18n): localize status labels in settings and stats (#11826)
* fix(i18n): localize status labels in settings and stats Status pills and summaries in Settings and Stats were built from bare string literals inside local helper functions, so they stayed English even when a language pack was active. Neighbouring copy in the same components already went through `translate()`, which made the panes look half-translated: "Универсальный доступ GRANTED", "GitHub ... Connected", Russian orchestration card titles above English summaries. The coverage audit does not catch this: it inspects JSX attributes and object properties, not values returned by helpers, so `verify:localization-coverage` stays green while the strings ship untranslated. Wrapped the remaining user-facing strings in `translate()` and let `sync:localization-catalog` add the 31 new keys. `computerUseSummary.*` already existed in `en.json` with identical copy but was never wired up, so those keys are now connected instead of duplicated. Moved the permission status helpers into `developer-permission-status.ts` to keep `DeveloperPermissionsPane.tsx` under the 400-line gate. Agent prompts in `orchestration-usage-examples.ts` are left in English on purpose: they are payload sent to the agent, not UI copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(i18n): use plural-aware message keys for settings labels Replace template-based pluralization (using {{value1}} for "s") with proper i18n plural forms following _one/_other suffixes. This enables correct pluralization across languages with distinct rules. Also extract duplicate integration status label translation logic into a helper function. * CodeRabbit's nitpick: the placeholder/plural assertions in settings-status-label-localization.test.ts only read en.json, so a translated catalog could ship a stale {{value1}} or a half-translated plural family undetected. Added a second describe block over the four shipped locale catalogs (es, ja, ko, zh), keeping the exact English-value assertions untouched and separate: --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
1e545f9c30 |
fix(speech): coalesce model download progress churn on the Settings/Voice path (#11962)
* fix(speech): coalesce model download progress churn A model download emits one state update per HTTP chunk (thousands for a 500MB model). Each one costs the renderer an IPC round-trip and a forced re-render of the speech-model menu, which stays open by design while a download runs — the Radix portal/Presence tree all four page.settings React #185 reports crashed inside. Emit at whole-percent granularity (what the UI renders) and keep the modelStates array identity stable when a refresh changed nothing, so a no-op refresh no longer forces a commit. Adds a speech_model_state_churn breadcrumb, registered in both coalescing sets, because no bundle in the cluster carried any speech telemetry to confirm a download was in flight. * fix(speech): quantise polled model state so download progress stops churning the renderer Adversarial round 1 found the renderer-side stabilisation was inert during a real download. The progress fan-out already coalesces to whole percent, but the renderer discards the event payload and re-polls getModelStates, and getModelState returned the cached downloading state verbatim - raw sub-percent progress. So every chunk produced a fresh object, resolveModelStates never matched, and all the real benefit came from the main-side coalescing alone. Quantise the polled reply to the precision the UI already renders (Math.round(progress * 100)), keeping the stored cache exact. Also from round 1: - the whole-status dedup swallowed downloadModel's already-downloaded branch, whose lone 'ready' is the only notification the requesting window ever gets - a dead click and a permanently stale pane with two windows open. Gate it on downloading -> downloading so every one-shot transition stays unconditional. - rename the storm test off '.react185.': it counts renders and asserts nothing about #185, and #185 could not be reproduced at IPC-realistic pacing. * fix(speech): stop the churn breadcrumb firing on every healthy download Adversarial round 2. CHURN_REFRESH_THRESHOLD was 60 per 5s window, but main clamps download progress at 0.9 and emits on whole-percent change, so one healthy download is capped at ~91 refreshes — and a 56MB model on a fast link lands all of them inside a single window. The breadcrumb fired on every normal download and burned a slot in the 30-entry ring it was coalesced into to protect. Raise it to 250 so it only fires on the per-chunk shape it was added to detect; noOpRefreshes in the payload still separates the two causes. Round 2 also found that every assertion round 1 added was one-sided (toBeLessThanOrEqual), so each of the three core behaviours could be mutated into "do nothing" with the whole suite still green: - suppress every downloading -> downloading event: progress bar frozen at 0% - toWholePercentState returning 0: every poll reports 0% - resolveModelStates never adopting a same-length change: the Voice pane never updates and a finished model never shows as ready The suite measured that the fix reduces work, never that it still does the work. Convert the two ceilings to exact series, assert the storm test's render floor as well as its ceiling, and add dictation-model-state-stabilisation.test.ts covering adoption per changed field, a full whole-percent download, and both sides of the churn threshold. Ruled out and deliberately not fixed: round 1's request-sequencing MEDIUM on refreshModelStates. 50 concurrent getModelStates() settle strictly FIFO over ipcRenderer.invoke at constant microtask depth, and migrationReady is assigned once in the constructor, so a monotonic request id would be dead code. * test(crash-reporting): cover speech churn breadcrumb name-coalescing The churn breadcrumb's registration in COALESCED_RENDERER_BREADCRUMB_NAMES and NAME_ONLY_COALESCED_BREADCRUMB_NAMES had no test: the storm test only asserted the constant equals its own literal. Removing either registration kept the whole suite green. It carries no message field, so without the name-only entry rendererBreadcrumbCoalesceKey returns undefined and every firing takes its own ring slot — the eviction this breadcrumb exists to avoid. * test(dictation): pin the churn threshold as a rate, not a lifetime total Both existing churn tests stopped at exactly 250 refreshes in one window, so two mutations survived: deleting the per-window reset (the counter degrades into a session total, and three healthy downloads at 91 refreshes each cry wolf), and === to >= (fires on every refresh past the threshold, flooding the ring). Pins Date.now rather than using fake timers so the window rule is measured, not the machine. * test(dictation): pin the churn clock and the window's lower bound The two 250-refresh churn tests measured real elapsed time against a 5s window, so a loaded runner that took longer than one window to run the loop would roll the window and go red. Pin Date.now in both. Pinning alone leaves CHURN_WINDOW_MS unpinned below: shrinking it 5_000 -> 50 kept all 13 tests green, yet a 50ms window can never accumulate 250 refreshes and the detector would be dead. Add a storm spread across most of one window so the constant has to be wide enough to hold a sustained storm, not just an instantaneous burst. Co-authored-by: Orca <help@stably.ai> * refactor(speech): narrow progress churn fix --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
4a76565a35 | fix(terminal): bound paired-client renderer work (#12081) | ||
|
|
006ce9d116 |
fix(dev): split the confirmation dialog so Fast Refresh can accept it (#11980)
* fix(dev): split the confirmation dialog so Fast Refresh can accept it `confirmation-dialog.tsx` exported both `ConfirmationDialogProvider` and `useConfirmationDialog`, so React Fast Refresh could never treat it as a boundary and Vite applied every edit to it in two passes under two `?t=` stamps. When a second file in the same subtree changed in one watcher batch, `createContext` ran twice and the provider published one context object while the consumer read the other — `useContext` returned null and the hook threw. Two field crash reports hit this at `ChecksPanel`, both dev-server sessions. The context and hook move to a new component-free `confirmation-dialog-context.ts`; `confirmation-dialog.tsx` keeps the provider and now exports only a component, so the refresh runtime accepts it. Not one line of the provider body changes — the 16 hook importers just point at the new module, `vi.mock` targets included, and `App.tsx` is untouched. * test(dev): pin the confirmation dialog Fast Refresh boundary The split that fixed the context-identity crash had no test behind it: no test imported ConfirmationDialogProvider, and the six vi.mock call sites replace the hook module wholesale, so they pass just as well with the provider and hook back in one file. Assert the module shapes the refresh transform actually keys on -- the context module registers no component, so it never gets an HMR footer to invalidate through. Co-authored-by: Orca <help@stably.ai> * test(dev): assert the refresh boundary on the module namespace The source-regex guard did not guard. Its patterns match only declaration forms, so `export { useConfirmationDialog } from './confirmation-dialog-context'` in the provider module -- which restores the crash, verified in a browser -- passed it 3/3. It also failed on a comment that merely contained the word createContext, and would fail on React 19's `<Ctx value={...}>` shorthand. Assert on the module namespace object instead, using react-refresh's own component criterion, so re-exports and default exports are visible. The third test renders the provider and resolves the hook through it, which is a real behavioural check rather than a shape one. Co-authored-by: Orca <help@stably.ai> * test(dev): classify boundary exports with the refresh runtime's own predicate The hand-rolled `^[A-Z]` name check called `export class Foo {}` a component; the runtime rejects any class whose prototype carries extra members, so that shape restored the two-pass split undetected. Use react-refresh's exported `isLikelyComponentType` and mirror `isCompoundComponent` instead of a third approximation. react-refresh was already resolvable only via shamefully-hoist, so it is now an explicit devDependency. * test(dev): tighten confirmation dialog boundary guard --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e58c051d16 |
fix(terminal): trust Pi CSI-u Shift+Enter on Windows (#9703) (#11769)
* fix(terminal): trust Pi CSI-u Shift+Enter on Windows (#9703) Pi enables the Kitty keyboard protocol at startup and decodes CSI-u, but TUI_AGENT_CONFIG['pi'] never set windowsShiftEnterEncoding, so on Windows Pi could only get CSI-u via the flaky live-KKP-flag path (isKittyKeyboardActivePane). After a tool ran a subprocess that emitted a reset sequence, the KKP flags dropped to 0, Orca sent Esc+CR, and Pi read it as plain Enter -> submit. It recovered on the next pane refocus. Set windowsShiftEnterEncoding: 'csi-u' for pi, mirroring the Droid fix (#7668), so the trusted CSI-u route covers Pi reliably independent of KKP-flag churn from tool subprocesses. * fix(terminal): complete Pi Windows CSI-u trust lifecycle * test(terminal): name foreground retry timing * test(git): accept bounded SSH remote probes --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
673d7ca926 |
refactor(relay): collapse the duplicated FrameDecoder into one shared module (#12078)
src/relay/relay-frame-decoder.ts and src/main/ssh/relay-frame-decoder.ts were 264 identical lines apart from one default: the relay logs decode faults to stderr when no handler is supplied, the SSH side stays silent. Two copies of framing logic is exactly where a wire-format fix lands in one and not the other. The decoder's contract and buffer already live in src/shared, so the class joins them there. The relay keeps a thin subclass that supplies its stderr default, preserving behaviour for the call sites that omit onError. The SSH copy is deleted and relay-protocol.ts points at shared directly. Verified: pnpm typecheck, 102 tests across the 9 framing/backpressure/ handshake suites, and `pnpm build:relay` for all six platform targets plus the WSL hook relay — the standalone bundle has no new dependencies. |
||
|
|
a14ada15e5 |
fix(test): restore Azure DevOps and Bitbucket remote-url probe assertions (#12080)
#12065 started passing an AbortSignal as a third argument to provider.exec in readRemoteUrl, and updated the Gitea, GitHub and GitLab assertions to match. Azure DevOps and Bitbucket were missed, so main is red on three test files. Same expect.any(AbortSignal) shape #12065 used for the other providers. Co-authored-by: Orca <help@stably.ai> |
||
|
|
3172002d71 |
fix(sidebar): preserve manual order during refresh (#12072)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
714bcbe43f |
fix(relay): make desktop control lifecycle provable and self-healing (#12076)
- RelayControlOrigin.activate rejects controls whose socket closed before activation (hello-ack and close in one ws parser turn previously published a dead control with no recovery path) - RelayControlClient gains a 75s inbound-silence watchdog mirroring the relay's ping contract, so dead or server-side-unindexed sockets terminate and trigger origin recovery - RelayAuthCoordinator only republishes 'registered' when the owned broker proves a live control, and logs reconcile failures instead of swallowing them; the origin pool logs recovery-attempt failures - Host-proof validation reports the failing check by name (never values), keeping main's 30s skew bounds - isLive() plumbed client -> origin -> pool -> broker Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
73c5009b82 |
chore(dead-code): drop ~2k lines of unreachable exports and orphan modules (#12077)
* chore(dead-code): drop 2k lines of unreachable exports and orphan modules Ran knip across every build entry (main, preload, renderer, popout, web, cli, relay, workers, forked sidecars, config scripts) and removed what no entry graph can reach. - 11 orphan modules nothing imported, plus one test that only covered them - 159 unused exports/types, with their now-dead helpers, imports and tests Each candidate was verified against dynamic references before deletion. 42 knip hits were false positives and are kept: shared modules consumed by the mobile/ workspace, the src/shared/plugins/** public API, vendored shadcn primitives, and relay wire-protocol constants held for compatibility. Adds knip.json + `pnpm audit:dead-code` so this stays measurable. Verified: pnpm typecheck, pnpm lint, and 2081 tests across the 73 affected test files all pass. * chore(dead-code): move knip config under config/ Root-level additions are blocked by the root directory guard. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
1562f12f78 |
fix(P1-D): coalesce remote-ref probes, TTL negatives, and bound unsettled keys (#12065)
* fix(P1-D): coalesce remote-ref probes, TTL negatives, and bound unsettled keys Keep forge resolution from stampeding git under worktree fan-out, let remotes added mid-session be discovered without a restart, and refuse pathological new-branch waves once the unsettled map is full. * fix(P1-D): stop abandoned probes publishing, and split capacity refusals A coalesced probe abandoned as stale kept running and still wrote its answer to the cache, so a late permanent miss could land over the successor's fresher one. Probes now publish only while they still own the in-flight key. The hosted-review capacity refusal told brand-new branches that an earlier attempt of their own never answered when the refusal was really the unsettled map or the process-wide detached cap; each cap now says what it is. Also caches stable "no such remote" SSH misses under the negative TTL instead of re-spawning the probe on every poll. Co-authored-by: Orca <help@stably.ai> * Bound SSH remote URL probe with deadline to prevent hangs The SSH branch of remote URL probes was unbounded — the relay's bounds are per-phase and reset on every frame, so a relay dribbling output would outlive them. Pass AbortSignal.timeout to the SSH provider's exec call to enforce the same 30s deadline as local probes. Treat AbortError as a transient probe error: it signals unavailable infrastructure (deadline or cancellation), not a negative answer about the remote. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
711491b40a | fix(agent-status): restore status in reused terminal panes (#12074) | ||
|
|
2fe655de72 |
Pr 9364 update (#11684)
* fix(workspaces): forget deleted remote mirrors
* fix(workspaces): tighten orphan cleanup guards
* fix(workspaces): avoid duplicate remote teardown after delete
* fix(workspaces): prevent orphaned filesystem auth on removal
When a worktree is deleted, especially from remote hosts, the filesystem
authorization cache was not being invalidated, leaving the path accessible
even though the workspace was gone. Use persisted host ownership to scope
cleanup to the correct partition and invalidate the auth cache when removing
a workspace to prevent orphaned authorization in host-partitioned scenarios.
* Fix orphaned worktree cleanup to trust persisted ownership and clean all
When a remote worktree or project is deleted, the local metadata cleanup must work even when the owning repo can no longer be resolved. The removal was incorrectly trusting a caller's potentially-stale hostId over the authoritative metadata, causing:
- SSH workspaces to be cleaned from only the local partition, stranding the remote partition with an un-bumped topology fence
- Sibling worktrees of the same repo to get rebased and lose unsaved tabs
- PTYs in orphaned workspaces to never stop when the selector can't resolve
- File watchers to keep firing events indefinitely
Now the cleanup trusts the persisted owner hostId, cleans all affected session partitions where tabs might live, intelligently gates topology fence bumps to avoid rebasing siblings, and passes the exact worktreeId to PTY sweeps that can't resolve the selector.
* Pass removal host ID to fix teardown of ownerless remote worktrees
When deleting an ownerless remote worktree, args.hostId may be absent.
Without an explicit host ID, the session teardown would incorrectly clear
the local session instead of the remote. Derive removalHostId from the
repo (the canonical owner) and pass it to every removeWorktreeMetadataAndTransientState
call to ensure the correct session is torn down.
* Scope worktree teardown to the owning host connection
- Orphaned SSH worktrees now sweep through the host's PTY provider instead of only the local one, so remote terminals die when the repo is gone
- Terminal ownership is scoped by resolved connection/runtime environment, preventing a same-id workspace on another host from being swept
- Persisted ownership beats stale live routing for in-flight keys and topology fences
- Renderer fails closed and never forgets a row whose removal route turns ambiguous mid-flight
* Fix worktree removal to scope session cleanup to the owning host
When a worktree is removed, its metadata purge must resolve the same owner
as the teardown sweep, or SSH/runtime partitions keep workspace state
forever. Additionally, materializing never-persisted host partitions
during removal can rebase sibling worktrees. Scope cleanup to owning host,
skip unwritten partitions, and detect transport-wrapped error codes that
Electron IPC re-wraps and strips causes from.
* Fix worktree removal to scope session cleanup to owning partition
- Only the owning partition may fence on emptiness; spill partitions
that never held the worktree must not claim repo authority to prevent
data loss when the renderer owns tabs elsewhere
- Tighten error code detection to require message boundaries (": " or
newline) instead of matching trailing tokens, preventing false
positives from triggering the destructive forget-local fallback
---------
Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
|
||
|
|
db69cd9387 | fix(agent-status): clear Claude question indicator after Escape (#12064) | ||
|
|
2f73775ffc | fix(terminal): bound fullscreen atlas recovery (#12061) | ||
|
|
25fefa4072 |
fix(P1-A): async SSH consumer-recovery persistence and detach on failed connect (#12026)
* fix(P1-A): persist SSH consumer recovery without a sync store flush rememberPtyConsumerRecovery ran on the live establish/reconnect path and called flushOrThrow -> writeToDiskSync, parking the Electron main thread on the profile-directory write. On a stalled or slow profile mount that freezes the whole app during SSH recovery and reconnect. Add Store.flushAsync(): same debounce-cancel and write serialization as flushOrThrow, but awaits writeToDiskAsync instead of blocking. The consumer recovery upsert/remove pair is now async and awaits it, and the SSH callers await through to establish()/reconnect() so ownership is still durable before relay setup continues. In-memory state still mutates synchronously (before the first await), so no caller can observe a torn record and dispose() stays synchronous. * fix(P1-A): detach the SSH session when a connect attempt fails Both failure exits in doConnect dropped the session from activeSessions without calling detach(). claimSshPtyConsumerRecovery only reuses an existing in-memory entry when detached === true, so the next connect attempt fell through to minting a fresh clientInstanceId, discarding the remembered owner lease and its resume identity. Route both exits through abandonFailedSshSession(), which detaches (keeping PTY ownership, unlike dispose()) before removing the session, and tolerates a teardown throw so it can't mask the connect error being rethrown. * fix(P1-A): await async lease persistence in SSH relay teardown Failed connect attempts now wait for 'detached' leases to persist before throwing, preventing reconnects from claiming them before cleanup completes. Detach and dispose operations are now async and await store durability. * fix(ssh): make session detach lease writes retryable on failure Separate in-memory detach (identity recovery, provider cleanup) from lease write persistence so rejected writes can be re-issued without re-running provider teardown or re-minting the session identity. Introduce flushDurableStateOrThrowAsync to flush only SSH-recovery state on the live establish/reconnect path, avoiding snapshot writes of sidecars that belong to quit/startup. Use Promise.allSettled in test reset to prevent one rejected disposal from leaking state into the next test. * fix(ssh): dispose mux on failed establish and propagate sync errors - Dispose mux when session is disposed during establish to prevent resource leak - Propagate synchronous errors in teardown via the completion promise instead of leaving completion undefined - Add test coverage for terminated PTYs that exit mid-reattach and must stay dead |
||
|
|
ce5b639e03 |
fix(P1-B): recover SSH targets and remote file watchers after a network drop (#12032)
* fix(P1-B): recover system-SSH targets after a network drop
Two defects stopped a remote workspace auto-recovering after a blip.
runReconnectAttempt classified failures with isTransientError, which only
matches ETIMEDOUT/ECONNREFUSED/ECONNRESET by errno code or literal
substring. The system-SSH transport — the only transport FIDO2 and
ProxyUseFdpass targets can use — reports network failures as OpenSSH
prose ("System SSH connection timed out"), so the ladder published a
permanent 'error' on the first timeout and the target never came back
without a manual reconnect. isTransientReconnectError adds a
network-shaped prose table on top of isTransientError and is used only on
the reconnect path: connect() keeps the narrow classifier so an
unreachable host still fails fast instead of burning five 30s attempts
and five security-key touch prompts. Auth and passphrase failures stay
permanent on both paths.
runReconnectAttempt also had no generation fence, so a superseded attempt
published its cancellation as a permanent error over the winner's live
connection — reachable when a system-transport proc.onExit schedules a
reconnect while an attempt is still in flight. Cancellation now carries a
stable error name, and both connect() and runReconnectAttempt claim their
connectGeneration and stay silent when a newer attempt owns the state.
* fix(P1-B): retry a dropped watcher overflow marker on real capacity
emitWatcherOverflowToClient published the {kind:'overflow'} resync marker
with controlOverflow:'reject'. A full control queue rejects at admission
with no settlement callback, so the marker was silently discarded and the
remote File Explorer stayed stale until some later watcher event happened
to produce another one — for a quiet tree, possibly never.
The emitter now retains a rejected marker per (client, root) and
republishes it when the sink actually frees up. The existing
onLegacyPtyCapacity signal cannot drive that: it is gated on producer
retention, so it stays silent exactly under the dual-queue pressure that
caused the rejection. RelayDispatcher.onClientCapacity is an ungated
per-client capacity signal that fires on every writer settlement and
drain. It lives on the dispatcher rather than the writer so a retained
marker survives setWrite() replacing the primary sink, and setWrite
notifies capacity once afterwards so the marker does not wait on traffic
that may never arrive.
Retention is bounded to one marker per (client, root), released on
settlement and purged on client detach.
* fix(P1-B): address all review findings on SSH network recovery
Fix four issues from code review:
1. **Bug — admitted overflow markers lost on setWrite**: Retain markers when
settlement fails `ok: false`, not just on admission rejection. Prevents
desynced filesystem trees after SSH sink replacement.
2. **SSH error classification expanded**: Add missing OpenSSH patterns
(`ssh_exchange_identification`, `connection closed by remote`) and new
`isDefiniteSystemSshHostFailure()` classifier.
3. **ControlMaster retry optimization**: Skip second probe when first failure is
already definite host-level (network timeout, refused, unreachable). Saves
~30s per reconnect ladder step.
4. **Overflow flush under dual-queue pressure**: Gate pending marker retries on
control-lane headroom instead of re-attempting on every capacity notification.
Reduces thrash proportional to producer traffic.
Add regression tests for marker republish on sink replacement and validate auth
error detection against live OpenSSH credential rejection messages.
* rm random doc
* fix(P1-B): skip credential-failure retries and recover watcher markers o
- Auth and passphrase errors fail immediately without retry attempts
- Bare "System SSH probe failed (exit 255)" is transient only for reconnect
- Watcher markers survive client invalidation when switching SSH connections
- Add network error patterns: "lost connection", "remote end closed"
|
||
|
|
ced4a2a959 |
fix(P1-D): bound hosted-review in-flight lookups so a wedged provider cannot pin a branch (#12030)
* fix(P1-D): bound hosted-review lookups with a detachable deadline The `inflight` map in the hosted-review branch cache was only ever cleared when the lookup settled, and nothing bounded how long that took. One wedged provider call pinned its branch for the life of the process: every later poll joined the same dead promise, so the card loaded forever with no in-session recovery. Each lookup now runs under a 120s deadline. Nothing below the funnel can be cancelled, so the deadline detaches instead: the record is released, the callers get the last known review (or a timeout error), and the branch enters the existing failure backoff. The lookup keeps running and its answer is still adopted if it lands, so a slow-but-alive host converges rather than failing forever. A token identity keeps a detached lookup from evicting the record that replaced it, and a wall-clock sweep expires records whose timer never fired — main's timers are suspended across system sleep. `inflight` is capped independently of the completed cache. The failure backoff moves to its own module: it has a different lifetime from the answer cache and is what a deadline records against. * fix(P1-D): bound `git remote get-url` on the local/WSL path `getRemoteUrlForRepo` ran the git child with no timeout, which is the one unbounded step under the hosted-review lookup funnel: `git/runner.ts` only arms its kill path when a timeout is passed, so a dead network mount or a stalled WSL interop hangs the call and everything above it. The SSH branch is already bounded by the relay mux's 30s request timeout, so it is unchanged. * rm review doc * rm review doc * test(P1-D): add probe tests and transient-failure recovery verification Add tests for coalesced-probe and remote-url-probe infrastructure. Add integration test verifying that transient Bitbucket API failures don't cache as a definitive no-review result, allowing recovery after cache TTL expiration. * fix(P1-D): track lookups from start, prevent stale scope adoption - Count unsettled lookups when they start, not after deadline expires: prevents multiple concurrent lookups for the same branch. - Add evicted generation floor: prevents adopting stale results when scope is invalidated and evicted from the map. - Consolidate duplicate repository reference cache logic into createRemoteRefProbeCache utility. - Fix deadline wrapper in git config signature lookup: bound the caller's deadline only, not the coalesced probe itself. * feat(P1-D): add remote-ref-probe-cache utility Cache successful remote URL probes per repo/runtime to avoid duplicate work. Skip caching transient errors and SSH failures so providers can retry on reconnect, preventing stale scope adoption during the session. |
||
|
|
6e2a88c091 | perf(worktrees): avoid redundant fetch during deletion (#11918) | ||
|
|
a20d82294b |
fix(agent-status): preserve Claude background work (#11838)
* fix(agent-status): preserve Claude background work * fix(agent-status): harden background task lifecycle * fix(agent-status): narrow interruption retention * fix(agent-status): scope background task authority * fix(agent-status): isolate lifecycle inventories * fix(agent-status): harden background evidence recovery * fix(agent-status): reject ambiguous child authority * perf(agent-status): skip lifecycle inventory scans * refactor(agent-status): isolate task inventory parsing * fix(agent-status): clear stale background evidence * fix(agent-status): gate accepted remote evidence * test(agent-status): pin session cron interrupts * fix: harden Claude inventory tracking * test: pin Claude cron drain authority * refactor(agent-status): unify Claude turn-boundary predicate Collapse the five inline copies of the Stop/StopFailure test into a single isTurnBoundary constant and drop the reportedStateName/stateName alias, so a future edit can't move one copy and leave the others behind. Pin the two behaviors that unification now depends on: a non-interrupted StopFailure keeps gating on live background work, and interrupted state does not survive a mid-turn lead event that has no prompt submit. Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): gate local Claude background evidence --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
4f963fd279 |
fix(browser): remove unsafe window close bypass (#12040)
* fix(browser): remove unsafe window close bypass * test(browser): strip legacy close policy on hydration |
||
|
|
9db4cde93b | fix(windows): keep browser close marker URL absolute (#12038) | ||
|
|
8ab85c9bfc |
fix(quit): stop durable state writes from parking the main thread on quit (#11931)
* fix(quit): stop durable state writes from parking the main thread will-quit ran stats.flush() and store.flush() synchronously, before preventDefault(). Both fsync and rename a multi-MB file on the profile directory. When that directory sits on a stalled network mount the syscall enters an uninterruptible wait: the app stops repainting and stops responding to Force Quit, because a process blocked in the kernel ignores SIGTERM and SIGKILL alike. The existing 20s teardown deadline could not bound this. Its timer runs on the very thread the syscall parked, so it never fires. The fix is to make the quit path awaitable rather than to try to bound it — a quit that is slow but responsive stays killable by the OS. - preventDefault() now runs first, so every teardown step is free to await - stats and state gain flushAsync() twins that use node:fs/promises - both join the existing teardown barrier, which can now actually bound them - the pass-2 will-quit re-entry returns early instead of re-running teardown - quitFlushStarted makes the quit flush the last write, so a teardown step touching the store cannot arm a debounce that races process exit Making the swap async cost the atomicity of check-generation-then-rename: a writer parked on await rename has already cleared the guard, so a later synchronous flush could be clobbered by stale state. Both async writers now claim their temp path, and the sync writers delete it, turning that swap into a swallowed ENOENT. Atomic temp+rename is unchanged, so a write cut short by the deadline leaves the previous file whole — bounded loss, never corruption. * fix(quit): harden async persistence finalization * fix(persistence): bound best-effort flushes |
||
|
|
de75003df9 |
fix(P1-C): gate FIDO2 system-SSH transport on an OpenSSH binary (#12029)
* fix(ssh): gate FIDO2 system-transport on an OpenSSH binary `ssh -G` echoes OpenSSH's built-in default identity list for every host, so `usesDefaultPaths` was almost never true and the security-key gate returned `!usesDefaultPaths || findSystemSsh() !== null` — forcing system transport without checking that an `ssh` binary exists. `spawnSystemSsh()` then throws `No system ssh binary found`, hard-failing connections that worked on ssh2. The same flag also stopped the default scan at the first existing normal private key, so a host that only accepts a FIDO2 key never reached system OpenSSH when `~/.ssh/id_rsa` happened to exist. Both decisions are independent of where an identity path came from: always require `findSystemSsh() !== null` before forcing system transport, and scan every candidate identity instead of stopping on the first normal key. `shouldUseSystemSshTransport()` is untouched, so ProxyCommand / ProxyJump / ProxyUseFdpass keep their intentional system transport. * test(ssh): isolate connection tests from the developer's own FIDO2 keys Transport selection now scans every default identity instead of stopping at the first normal key, so a `~/.ssh/id_ed25519_sk` on the machine running the suite would decide which transport the default-target tests take. Mock `findSystemSsh` to null by default and opt the two security-key tests in. |
||
|
|
dbfffa6530 |
Add first user prompt to AI Vault session history row (#12006)
* Add first user prompt to AI Vault session history rows Re-parse transcripts on demand to extract and display the untruncated first user prompt for copy/reuse. List scans omit the body (payload/perf); UI loads it when session details expand. Grok sessions extract the typed ask from <user_query> envelope, skipping injected <user_info> bootstrap rows. Supports Claude, Codex, Grok, and OpenCode agents. * fix(ai-vault): split SessionTime out to pass max-lines lint AiVaultSessionDetails exceeded the 400-line oxlint limit after adding first-prompt UI; move SessionTime into its own module. * fix(ai-vault): handle corrupt transcripts and fix OpenCode prompt captur Corrupt transcripts now resolve null instead of rejecting the IPC call, matching behavior for other unavailable cases. OpenCode SQLite parsing now correctly captures all text parts from the earliest user message only, fixing truncation of large prompts and padding of small ones. Add stale-response guard in the UI to prevent late results from overwriting the current session when tabs switch. Consolidate text slicing via `sliceAtCodeUnitLimit` to avoid surrogate-pair splits across all callers. * test(ai-vault): add first-user-prompt UTF-16 safety tests Ensure truncation at safety limits doesn't split UTF-16 surrogate pairs, preventing corruption of astral characters in captured prompts. * fix(ai-vault): key first-prompt-card by session.id Remounting the card on session switches prevents late responses from a previous load from writing stale data into the component's refs. Also improves conversation-turn key stability. |
||
|
|
8fc892dd02 | fix(i18n): add missing Editor Font Family translations for es, ja, ko, zh (#11573) | ||
|
|
2f104d8713 |
Tier GitHub PR lookup polling to prevent quota exhaustion (#12013)
* Tier GitHub PR lookup polling to prevent quota exhaustion The selected worktree (O(1)) checks per-minute; card list (O(N)) per-15-minutes. Introduce process-wide cache to collapse concurrent polling and gate lookups on available rate-limit budget with exponential backoff on failure. - Preserve last-known review during backoff - Invalidate cache when Orca opens a PR - Stop coordinator from double-charging * Tier GitHub PR lookup polling to prevent quota exhaustion - Return the latest reset time when both GitHub API buckets are rate-limited, preventing premature retries against still-blocked buckets. - Serve the last known review on transient lookup failures, preventing reviews from blinking out on temporary errors. - Discard in-flight lookups that predate an invalidation so stale answers cannot overwrite newly opened reviews. * fix: give rate-limit reset tests unique titles oxlint vitest/no-identical-title was failing static analysis because two cases shared the same describe title. |
||
|
|
a7d769e13d |
Watcher explorer relay regressions (#12012)
* fix(watch/relay): bound remote watcher fan-out and read the live relay grace Three P1 fixes from the SSH/remote freeze audit: - Remote watchers now debounce on the same 150/500 window as local ones (finding D), and every teardown path drops the trailing flush timer instead of letting it fire into a dead watch. The deferred send is wrapped so a frame disposed mid-window can't escape as a fatal main-process exception. - File Explorer refreshes are scheduled and concurrency-capped rather than fanned out unbounded over expanded dirs (finding C). Local transports use a zero window, since main already coalesced the burst. - relay.startGrace reads ptyHandler.configuredGraceTimeMs instead of the launch-time argv closure, so a grace raised after launch is honored. The branch selection moves to relay-grace-branch.ts because relay.ts has no exports and calls main() at import, making it untestable. Consequence: a host-sleep relay holding zero PTYs now exits after the idle cap. Pinned by test and documented in docs/reference/relay-grace-time-reconfiguration.md. Also drops the duplicated 150/500/5000 constants in the runtime-RPC batcher in favor of the shared window module. * docs(relay): correct grace-reconfiguration line numbers after the relay.ts edit Co-authored-by: Orca <help@stably.ai> * refactor(file-explorer): use useMemo for paths; remove relay reference Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle. * rm design doc * fix(watcher/explorer/relay): coalesce POSIX paths by byte identity; pres - Remote watcher event coalescing now keeps NFC/NFD-distinct POSIX paths separate while still folding Windows path spellings, fixing cache invalidation when recreating directories with different Unicode compositions. - Relay grace reconfiguration now preserves shutdown-deferred state when grace is set to zero, preventing premature shutdown when the grace timer is reconfigured mid-flight. - File explorer refresh refactored from fixed-wave batching to concurrent task execution with `forEachWithConcurrency`, batching results every N settled reads instead of every wave, and reporting whether cancellation discarded pending work. - Watch handler now resyncs when events arrive after disposal, ensuring refresh requests aren't lost when events race cleanup during worktree switches. * fix(watcher/explorer): resilient commit batches on callback error Cache writes precede callbacks, so a throwing callback cannot strand the batch with stale marks. All callbacks complete despite errors, with the first error thrown after. - Fix commitBatchSize calculation for empty dirs - Add scheduler discard-report test - Move disposed variable before handleFsChanged - Expand batching-strategy comments * test(activity): stabilize portal readiness latch release under CI load The fixed 9-flip budget could miss enough MutationObserver deliveries under shard load for sibling DOM to still report loading. Drain readiness rAF after each flip and wait until latched unavailable is actually observed. * fix(watcher/explorer): stop batches on commit error; verify drain settle Enhance resilience under CI load and error conditions: - Portal readiness drain now returns a boolean confirming settlement; test expects verify the drain completed before proceeding. - File explorer commit batches stop executing after a callback throws, preventing stale writes after caller-observed rejection. - Watch hook requires worktree ID upfront in effect guard, eliminating redundant checks inside loops. * fix(relay): re-arm grace on configured change Refactor grace reconfiguration into a dedicated decision function. startGrace samples the configured grace at arm time, so a raised value landing mid-window would still fire at the old deadline without re-arming. Extract the logic with proper tests to ensure shutdown-deferred state is preserved across re-arms. * refactor(relay): extract grace reconfiguration to testable function Extract `applyRelayGraceTimeConfiguration` so the grace-time re-arming logic can be tested independently. relay.ts runs `main()` on import and exports nothing, making the call site (including retryDeferredShutdown hand-off into startGrace) otherwise untestable. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
c5c10e1203 |
Fix POSIX path coalescing and file explorer watch regressions (#12004)
* fix(watch/relay): bound remote watcher fan-out and read the live relay grace Three P1 fixes from the SSH/remote freeze audit: - Remote watchers now debounce on the same 150/500 window as local ones (finding D), and every teardown path drops the trailing flush timer instead of letting it fire into a dead watch. The deferred send is wrapped so a frame disposed mid-window can't escape as a fatal main-process exception. - File Explorer refreshes are scheduled and concurrency-capped rather than fanned out unbounded over expanded dirs (finding C). Local transports use a zero window, since main already coalesced the burst. - relay.startGrace reads ptyHandler.configuredGraceTimeMs instead of the launch-time argv closure, so a grace raised after launch is honored. The branch selection moves to relay-grace-branch.ts because relay.ts has no exports and calls main() at import, making it untestable. Consequence: a host-sleep relay holding zero PTYs now exits after the idle cap. Pinned by test and documented in docs/reference/relay-grace-time-reconfiguration.md. Also drops the duplicated 150/500/5000 constants in the runtime-RPC batcher in favor of the shared window module. * docs(relay): correct grace-reconfiguration line numbers after the relay.ts edit Co-authored-by: Orca <help@stably.ai> * refactor(file-explorer): use useMemo for paths; remove relay reference Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle. * rm design doc * fix(watcher/explorer/relay): coalesce POSIX paths by byte identity; pres - Remote watcher event coalescing now keeps NFC/NFD-distinct POSIX paths separate while still folding Windows path spellings, fixing cache invalidation when recreating directories with different Unicode compositions. - Relay grace reconfiguration now preserves shutdown-deferred state when grace is set to zero, preventing premature shutdown when the grace timer is reconfigured mid-flight. - File explorer refresh refactored from fixed-wave batching to concurrent task execution with `forEachWithConcurrency`, batching results every N settled reads instead of every wave, and reporting whether cancellation discarded pending work. - Watch handler now resyncs when events arrive after disposal, ensuring refresh requests aren't lost when events race cleanup during worktree switches. * fix(watcher/explorer): resilient commit batches on callback error Cache writes precede callbacks, so a throwing callback cannot strand the batch with stale marks. All callbacks complete despite errors, with the first error thrown after. - Fix commitBatchSize calculation for empty dirs - Add scheduler discard-report test - Move disposed variable before handleFsChanged - Expand batching-strategy comments --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a07427e970 |
fix(ssh, relay): keep remote sessions alive through reconnects and backpressure (#11999)
* fix(ssh,relay): stop remote connections from being killed by backoff and frame caps Three independent connection killers found in the SSH/remote freeze audit. FINDING A - the reconnect ladder never escalated for post-handshake drops. scheduleReconnect() used the single published state.reconnectAttempt for both the delay index and the give-up test, and runReconnectAttempt() zeroed it before connecting (ssh.ts gates the relay redeploy on 0-at-connected). Every post-handshake drop therefore re-entered at 1000ms forever, ~3600 relay redeploys/hour, and 'reconnection-failed' was unreachable for a flapping host. New SshReconnectLadder splits the delay index (advanced by every retry) from the failure streak (advanced only by a failed handshake), so flaps back off while give-up semantics stay byte-identical to shipped. FINDING B - notify() closed the client whenever a frame exceeded the producer frame capacity, conflating a permanently un-sendable frame with transient backpressure. A 5000-event fs.changed is 425KB against a 49KB cap, so the watcher flood killed the link and re-killed on every reattach+replay. notify() now drops and logs once per generation; fs.changed is chunked to each sink's capacity with a control-lane overflow marker as the resync fallback; agent-hook envelopes shed lastAssistantMessage/interactivePrompt/subagents to fit. FINDING B2 - sendResponse routed >1MB responses to a lane whose admission ignores the frame cap and closed the client on rejection, so a large fs.listFiles dropped the SSH host. It now substitutes a JSON-RPC error so the request fails instead of the connection. Also moves fs.streamEnd/fs.streamError to the control lane so a terminal frame cannot be dropped by the producer-lane check. Co-authored-by: Orca <help@stably.ai> * fix(relay): stop the overflow marker from re-killing the link it protects Round-1 review fixes on the P0 freeze work. The control-lane overflow marker could reinstate the exact failure this P0 removes: dispatcher-client-writer closes the client when control-lane admission fails, and admitControl is the only lane that returns an error, so one marker per failing batch accumulated to the 256-frame/1MB bound and dropped the link. Markers are now deduped to one outstanding per (client, root), cleared on settle. Chunking also defeated the renderer's per-payload directory dedupe -- events are now stable-grouped by parent directory so one directory lands in one chunk -- and the halving walk overshot the byte minimum ~1.7x while the fast path paid three JSON encodes; both are fixed by publishing first and sizing from a measured bytes-per-event estimate. Agent-hook shedding now surrenders the blocking interactive prompt LAST rather than first, so a degraded envelope cannot strand a pane at state=waiting with no answerable question card. The dropped-notification log now distinguishes over-capacity from producer queue backpressure and no longer lets the first dropped method silence every other producer for the life of the connection. * fix(relay,ssh): keep status delivery and terminal frames from trading one freeze for another Round-2 review fixes. The round-0 change from close-on-rejection to silent drop removed the only redelivery path for agent.hook envelopes: they are fire-and-forget and the per-pane cache only replays on handler install, so a saturated link stranded a pane on a stale Working spinner until reconnect. Closing used to guarantee delivery by forcing that replay. Envelopes now publish per client and pend for bounded latest-wins redelivery when the producer queue rejects them. Shed fields are now named on the wire. The subagent roster is not cosmetic -- the renderer replaces rather than merges it, and hibernation gates on its length -- so an unmarked shed could sleep a live pane. fs.streamEnd rode the control lane because it must not be dropped, but that lane kills rather than drops. The stream's concurrency slot is now held until the terminal frame settles rather than until the fd closes, capping queued terminal frames well under the control budget; overflow costs one refused read instead of the connection. The watcher chunk walk now stops while producer retention sits past its reserve and degrades to a resync, so a 5000-event flood cannot fill the queue that interactive PTY traffic shares and stall every remote terminal. The reconnect ladder caps its flap-path delay so delay plus handshake timeout cannot cross the relay grace floor and let the remote daemon kill live PTYs. Also: the suppression key no longer embeds a NUL byte, which had made the file binary to git and grep; producerEnvelopeBudget no longer reports infinite capacity for a departed client; the drop logger no longer encodes a frame it will not log; and an over-capacity response substitution no longer settles as if the result had been delivered. * fix(relay,ssh): restore relay-shed status fields and scope backpressure per client Round 3 + 4 review fixes. Watcher chunking is now gated on the *client's* retention reserve rather than the dispatcher-wide one, so one stalled peer no longer forces a healthy client into a full file-tree resync. The relay-lost redeploy ladder no longer burns its 6-attempt budget while the SSH transport itself is down: it holds at the 15s step with a non-terminal status and rearms, so a laptop that slept past the ladder comes back instead of landing on a terminal "give up" banner. The shedFields wire marker had no consumer, so an agent-hook envelope whose subagent roster was dropped to fit the frame read as "roster cleared" on the Orca side: live child rows blanked and a done pane became hibernation-eligible while its teammates were still running. ingestRemote now restores shed fields from the cached payload (interactivePrompt deliberately excluded — a stale answerable question card is worse than none). Also: stream terminal-frame slots are counted per client, since the control queue they protect is per client; the chunking fast path no longer logs a drop for a batch it goes on to deliver in full; -32010 is now RelayErrorCode.ResponseOverCapacity. Test debt from the review: pending-pane eviction, per-client stream isolation, and the reconnect budget are now asserted rather than assumed; four fragile exact-byte pins dropped in favour of the tier comparisons that carry the requirement. * fix(relay,ssh): restore relay-shed status fields and scope backpressure - Oversized relay responses now fail their request instead of closing the connection, preventing one frame from killing every pane on the host - Restore subagent state for correct hibernation; don't resurrect stale prose across turns - Account for relay re-establishment and PTY reattach time in SSH flap delay caps - Only log drops of final unsendable envelopes, not temporary rejections during measurement probes - Fix watcher overflow marker release race when notification admission rejects without settlement; use precise byte counting for event batching * Restore relay-shed fields with digest validation and scoped backpressure Validate that shed subagent rosters match their wire digest and turn identity before restoration, preventing stale roster resurrection. Compact interactive prompts for waiting states instead of dropping them. Demote control-queue overflow to non-fatal rejection so clients can retry on capacity recovery, keeping the link alive during transient backpressure. * fix(relay): correct ResponseOverCapacity error code ResponseOverCapacity should use -33008 to stay in the -33xxx range for relay protocol errors, not -32010. * fix(relay): close client when pty.replay overflows control queue Replay is never retried, so it uses the control lane where overflow is fatal — the writer closes the client and reconnect reloads history rather than stranding a short buffer. * fix(relay): prevent infinite redeploy on flapping SSH transports Charge reconnect attempts when connection restores mid-backoff, preventing infinite loop on transports that flap between states. Refactor control overflow handling to use entry property instead of WeakSet marker for clarity. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
05206046f6 |
chore: condense code comments (#12008)
* chore: condense code comments * chore: shorten more code comments * clarify PTY agent session descendant cleanup behavior Refine the comment on ptyAgentSessionIds to more accurately describe when agent sessions sweep their descendant process trees and note the exception on immediate Windows shutdown. |
||
|
|
2b44e9ed9e |
fix(updater): notarize hourly macOS builds so TCC grants survive updates (#12007)
macOS anchors a notarized Developer ID app's TCC grants on identifier + team, which is cdhash-independent and so survives an in-place update. Without a notarization ticket there is no such stable identity, so every hourly reads as a different client: the grant row stays but stops matching, and file access under Documents/Desktop/Downloads fails with EPERM and no re-prompt. `tccutil reset` fixes it until the next build — and orca-hourly has shipped as many as 14 builds in a day. Skipping notarization was chosen because Squirrel.Mac validates the replacement bundle's signature, not its notarization. That is true, but it is the wrong requirement; the in-place swap was never the problem. Budgets grow to absorb the notary round trip (publish 2x45, job 150), and the App token is re-minted after the build so its one-hour life starts at the first call that uses it rather than during `pnpm install`. |
||
|
|
36cc8495ef |
fix(terminal): stop the active-terminal repair loop from tripping React #185 (#11950)
* fix(terminals): make redundant tab activation idempotent (React #185) setActiveTab always reallocated activeTabIdByWorktree, even when the tab was already active for that worktree. Terminal's active-terminal repair effect depends on that map, so when the repair cannot converge activeTabId -- which happens when an earlier-scanned worktree reuses the tab id -- the effect re-triggers itself every commit until React throws #185. Crash cluster A: 12 reports, boundary terminal.workbench, 1.4.162/1.4.163. Co-authored-by: Orca <help@stably.ai> * fix(terminals): converge activeTabId when a tab id is owned by two worktrees Prefer the active worktree when resolving a terminal tab's owner. First-match ownership left activeTabId permanently unconvergeable under a duplicated tab id, so the active-terminal repair effect re-triggered itself into React #185. Breadcrumb the duplicate-ownership state (once per tab id) so a crash bundle can prove or kill the production origin of the precondition. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): coalesce the duplicate-tab-owner breadcrumb Its renderer guard is once-per-tab-id, so the stale worktree map it exists to diagnose duplicates every tab id at once and could evict the whole 30-entry ring. Also drops two keyed re-reads of tabsByWorktree that would throw for a prototype-named worktree id, and pins the activeTabIdByWorktree guard with a test that fails without it. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): key the duplicate-tab-owner crumb on its convergence flag Name-only coalescing keeps only the newest payload, so a resolvedToActiveWorktree false sample — the one value saying the activation still could not converge — was erased by any later benign true in the same 30s window. Keys on the flag instead, mirroring the WebGL name:kind branch; two keys still bound the burst. Also: the previous coalescing commit had no test at all (removing the name from both sets broke zero of 2701 tests), the resolver's activeWorktreeId truthiness check was a hole rather than a guard for a '' active id, and the resolver test file failed oxfmt --check. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): keep the non-converging duplicate-tab verdict The two earlier commits contradicted each other. Splitting the coalesce key existed so a `false` verdict could not be erased by a later benign `true` — but the renderer guard was keyed on the tab id alone, so for any one id only the first verdict was ever emitted. A duplicated id that first resolves benignly, then stops converging when the user switches worktrees, dropped the `false` sample at the source. That sample is the whole reason the breadcrumb exists: it is the only value saying the activation could not converge activeTabId. Key the guard on id plus verdict. At most two crumbs per tab id, and the main process still folds each verdict into its own ring entry, so the flood bound is unchanged. * fix(crash): correct the duplicate-tab verdict rationale, pin and cap the guard Three comments said `false` is the verdict that matters because it is the only one showing the activation could not converge. That is backwards. The repair effect activates a tab drawn from tabsByWorktree[active], so the React #185 path can only ever emit `true`; `false` is what a deliberate jump-to-agent into a background worktree emits from a fully converged state. A reader of the next bundle would have discarded the exact sample the breadcrumb exists to capture. The mechanism was right, only its stated reason was wrong: the real justification for keying on the verdict is symmetric, since coalescing keeps only the newest payload and either verdict would erase the other. The suite also did not pin the "at most 2 per tab id" bound - a guard keyed on `${tabId}:${activeWorktreeId}` passed all 11 tests while emitting once per worktree, the storm the guard exists to prevent. Adds a count-pinning test that kills it. Caps the never-pruned guard set at 256 distinct verdict keys (~85KB), mirroring MAX_COALESCE_KEYS. Measured 330 B/entry; a realistic thousand duplicated tab ids is ~0.6MB, negligible but unbounded in principle. * perf(crash): scan worktree tabs by key, and soften the verdict rationale Round 6 corrected my own round-5 comment. I had written that `true` is the repair-loop signature and `false` covers a deliberate background activation. The repair effect can emit `false` too: its closure holds the worktree from its render while the guard runs against live state, so a worktree switch landing in between reattributes the tab. The verdict hints at the caller; it does not prove it, and neither value should be discarded. Comment-only. Also take the free scan win the perf review measured: Object.entries allocates a pair array per worktree on a path that runs per tab activation. Own keys are safe to index by, so Object.keys plus an indexed read is behaviour-identical (16.1us -> 5.0us at 170 worktrees x 10 tabs). * fix(terminal): keep a duplicated tab id from re-sorting the active worktree setActiveTab now prefers the active worktree when a tab id is held by more than one, but terminals.ts has a second, older owner resolver: getTerminalTabOwnerWorktreeId, a memoized map built last-writer-wins. Two of its callers — setRuntimePaneTitle and clearRuntimePaneTitle — use the result for the same "is this pane in the active worktree" gate, so under a duplicate the two resolvers disagree: the cache names whichever worktree it saw last, which can be a background one for a pane the user is looking at. The gate then fails open and every classified OSC title frame bumps sortEpoch, reinstating the click-driven sidebar re-sort #209 removed — 20 title frames measured 20 bumps, each one a store write that re-renders every sortEpoch subscriber. isTabInActiveWorktree answers from the active worktree's own tab list instead of a tie-break. It stays behind the cheap id equality so the common non-duplicated path is unchanged, and it is a hasOwn lookup plus one scan of that worktree's tabs rather than resolveActiveTabOwnerWorktreeId, whose full scan would run per title frame and whose breadcrumb would fold a second caller into one verdict. Leaves updateTabTitle and clearTabLaunchAgent on the cache: they pick which copy of a duplicated tab to mutate, where no answer is defensible until the duplication itself is fixed. * test(terminal): pin the SSH-hydration origin of the duplicate tab id Drives the duplicate from real hydration rather than constructing it: a direct-SSH snapshot is keyed by worktree path, so renaming the worktree on the host (or re-adding the repo, which mints a fresh id) re-resolves it to a new worktree id while replaceHydratedRecordKeys retains the old key verbatim. Nothing de-dupes across keys. Fails on unfixed origin/main with converged=false after 200 passes; the two precondition assertions pass on both sides, so the red is the non-convergence itself and not a setup divergence. The reconnectPersistedTerminals stub is load-bearing and marked as such: with no registered PTY the orphan sweep cleans the duplicate up before the repair effect sees it. * docs(test): record the end-to-end #185 reproduction method on the regression test Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the null-active-worktree guard in isTabInActiveWorktree Dropping the `activeWorktreeId === null` early return was killed by nothing: `Object.hasOwn(map, null)` coerces to the string key 'null', so a worktree literally named 'null' would answer for "no active worktree". An untested guard reads as dead code and gets deleted. * rm triage context * rm triage context * rm context files * refactor(terminal): extract repair logic into reusable hook and guard ag Extract the active terminal repair effect from Terminal.tsx into `useActiveTerminalRepair` hook to enable reuse in tests and clarify responsibilities. Replace falsy coercion guards (`obj[id] ?? []`) with explicit `Object.hasOwn()` checks to handle edge cases: empty-string worktree ids (valid but falsy), prototype-named ids like 'toString', and duplicated tab ids across worktrees. Remove the now-unused `isTabInActiveWorktree` helper. Simplify the isActive logic in terminals.ts to rely solely on owner-equality since the repair now uses proper membership checks. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
036b1e78ba |
fix(terminal): add replacement policies for repeated same-handle stream (#12003)
Recovery logic strengthened with replacement-policy tiers (reuse/prefer-replacement/require-replacement) and bounded same-handle end cycles. Prevents infinite flapping by capping reuse attempts and inventory wait windows. Tracks ready evidence to reattach from prior snapshots when inventory becomes unavailable. |
||
|
|
786d7048a1 |
fix(win32): suppress Command Prompt window on IDE launches (#11907)
* fix(win32): suppress Command Prompt window on IDE launches - Prefer JetBrains GUI executables (`*64.exe`) over `.cmd` shims to avoid console allocation (STA-3040). - Use `start "" /B` when launching GUI apps via batch scripts; shims chain through console helpers that allocate a visible prompt even with `windowsHide`. `start /B` returns immediately, preventing the lingering window. * fix(win32): suppress Command Prompt window on IDE launches Prevent lingering Command Prompt windows when launching JetBrains IDEs on Windows. Use `start "" /B cmd /d /c` so the nested shell exits with the batch script, but only for JetBrains shims—VS Code and Cursor keep the waiting form because `start` re-parses arguments and breaks remote paths with spaces. Prefer colocated `*64.exe` executables beside the resolved `.cmd` shim over PATH lookups to avoid stale installations. * fix(win32): extend IDE launcher console suppression to direct paths Support IDE paths stored directly in settings (e.g., idea.exe, webstorm.cmd). Detect console idea.exe stubs alongside batch shims for upgrade to GUI *64.exe. Fix start command title escaping: use empty string instead of '""' to prevent libuv re-quoting. |
||
|
|
b04c695750 |
fix(runtime): drop stale local agent rows from worktree.ps after tab close (#11464)
* fix(runtime): drop stale local agent rows from worktree.ps after tab close attachAgentRowsToSummaries attached every hydrated hook row by worktreeId with no check that the pane/tab still exists, so agents from closed tabs (last-status.json hydrates for days) kept showing on mobile as current activity. Local rows now require the tab in a session/runtime graph or a connected PTY; remote rows are exempt since their tabs may only exist on the remote host. Fixes #6072 * fix(runtime): resolve legacy numeric pane keys through the stale-row filter Non-UUID leaves produce tabId:paneRuntimeId keys with no tabId field; without parsing them the stale filter was bypassed entirely for such rows. * fix(runtime): filter stale WSL agent rows * fix(runtime): ignore persisted tabs for agent liveness * fix(runtime): restore session-tab liveness and thread OSC transport through the stale-row filter Review loop pass 1 (3 independent same-model reviewers, findings converged): - Revert a829e8f9cf's `!this.tabs.has(tabId)` to `mirroredWorktreeId === undefined`. The renderer graph is structurally empty under headless serve (index.ts publishes {tabs: [], leaves: []}), is cleared by markGraphUnavailable, and omits unvisited/cold-parked workspaces, so graph-only existence dropped live agent rows in all those states and broke worktree.ps/session.tabs.list parity. Every close path prunes the persisted tab, so session tabs remain valid liveness evidence; the stale-persisted-tab premise did not survive tracing. - Restore the rename and legacy-pane-key tests to their session-only fixtures (the graph syncs added with the flipped predicate masked the contract change) and pin the restored contract in a named test. - Thread the pane's connectionId through RuntimeAgentRowSnapshot so OSC-retained rows keep the SSH exemption; previously a fresher OSC ping hardcoded null and stripped it. - Pin each rescue conjunct individually (paneKey-only, tabId-only, ptyId after binding clear), the WSL keep direction, the unresolvable-paneKey guard, and row presence in the freshness cases. * fix(runtime): carry the OSC-observed ptyId when a hook row wins the freshness race Hook payloads have no ptyId field, so overwriting the rowSources entry discarded the OSC-observed one and the connected-PTY ptyId rescue went dead for hook-fresh panes during a binding-clear window (pass-2 review P3). Also corrects the incarnation-change comment on the OSC rescue test. |
||
|
|
5c0195af64 |
Bound remote watcher fan-out and defer File Explorer refreshes (#11908)
* batch remote watcher events and defer File Explorer refreshes Remote filesystem watcher events now batch with the shared 150ms trailing and 500ms max-wait window, coalescing per-path like local events. File Explorer tree and directory refreshes are scheduled with debounce and transport-aware concurrency caps (16 local, 8 runtime, 4 SSH). Stale directory cache tracking prevents trusting collapsed listings skipped by full refresh; they are re-read on re-expansion. Relay implements a 15-minute idle-only grace cap for zero-PTY relays via PTY pool lifecycle tracking, independent of explicitly configured grace time. * fix(watch/relay): bound remote watcher fan-out and read the live relay grace Three P1 fixes from the SSH/remote freeze audit: - Remote watchers now debounce on the same 150/500 window as local ones (finding D), and every teardown path drops the trailing flush timer instead of letting it fire into a dead watch. The deferred send is wrapped so a frame disposed mid-window can't escape as a fatal main-process exception. - File Explorer refreshes are scheduled and concurrency-capped rather than fanned out unbounded over expanded dirs (finding C). Local transports use a zero window, since main already coalesced the burst. - relay.startGrace reads ptyHandler.configuredGraceTimeMs instead of the launch-time argv closure, so a grace raised after launch is honored. The branch selection moves to relay-grace-branch.ts because relay.ts has no exports and calls main() at import, making it untestable. Consequence: a host-sleep relay holding zero PTYs now exits after the idle cap. Pinned by test and documented in docs/reference/relay-grace-time-reconfiguration.md. Also drops the duplicated 150/500/5000 constants in the runtime-RPC batcher in favor of the shared window module. * docs(relay): correct grace-reconfiguration line numbers after the relay.ts edit Co-authored-by: Orca <help@stably.ai> * refactor(file-explorer): use useMemo for paths; remove relay reference Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle. * rm design doc * fix(remote-watcher): prevent stranded timer after close An in-flight provider receive can land after the batch is torn down. Without a guard, pushing events to a closed batch would re-arm a timer that would never be cleared, stranding the task indefinitely. Track the closed state and skip pushes after close(). Relay.ts comment clarifies why pool watches remain registered during grace-period shutdown deferral — the socket server stays listening so a reconnecting client can cancel the grace and resume. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
16c5526dfd |
fix(daemon): cover in-flight sleep in PAM watch (#11921)
* fix(daemon): rebaseline in-flight PAM suspension * test(activity): await portal readiness commits |
||
|
|
33c14bc716 |
fix(ssh): fall back to OpenSSH for FIDO2 keys (#11913)
Closes #11645 |
||
|
|
1f307afa6d | fix(terminal): preserve follow output through streaming refocus (#11915) |