mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fb724f630cbaecdf359a36c702c19361637b9160
11043
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fb724f630c | test(mobile): pin hyperlink payload from clean mobile dependencies | ||
|
|
1f3657cac1 | fix(terminal): close hyperlink sweep correctness gaps | ||
|
|
cd696ce245 | fix(terminal): disable marker row shortcut after reflow | ||
|
|
ae64821ae6 | test(terminal): cover hyperlink cleanup after buffer movement | ||
|
|
7e990eda24 | perf(terminal): scan OSC links only on marker rows | ||
|
|
5e984fd093 | test(mobile): verify hyperlink-cleanup WebView payload | ||
|
|
ca4acd73c2 | fix(terminal): retire overwritten OSC hyperlink metadata | ||
|
|
357c9780f8 |
refactor(agent-launch): retire the duplicate worker-start mode decision (#20911)
`orchestration-worker-start-mode` becomes a thin adapter over
`agent-launch/agent-launch-mode`, which already owns the same decision.
Orchestration keeps its receipt vocabulary via WORKER_START_VOCABULARY, so
every sentence a dispatch receipt prints is unchanged.
Recovers the cutover written in
|
||
|
|
e6a41081a3 |
fix: name the id-kind mismatch when --ack is given a message id (#15743)
* fix: name the id-kind mismatch when --ack is given a message id
`orchestration check --ack` takes the batch's delivery id, which the check
response returns as the top-level `deliveryId`. Passing a message id
instead produced:
stale_delivery: Delivery msg_5d5cdf77614c does not belong to this Run.
That states a Run-scope verdict for what is really the wrong kind of
identifier, and it names no field that carries the right one. The only id
visible while reading the message list is the message `id`, so the message
sends the caller hunting the wrong axis — #15697 is a detailed report that
concluded the ack path was broken and no delivery id was exposed, when both
were fine.
When the value misses the deliveries table but hits the messages table, say
so and point at `deliveryId`. Anything else keeps the original wording,
including a delivery that exists but belongs to another Run — that one
really is a scope verdict.
Refs #15697
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: bind message-id diagnostics to queued rows
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
7f23d4463d |
refactor(daemon): consolidate snapshot-safe listener delivery (#20945)
Consolidate snapshot-safe daemon PTY listener delivery and exit payload construction. Reuse listener removal and event types while preserving callback ordering, payload isolation, optional exit fields, and recovery exception handling. Fixes #10984. Adapted from #11119. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com> |
||
|
|
07c7606fee |
fix(computer-use): reap detached macOS helpers through owner exit (#20926)
Reclaim detached macOS computer-use helpers on abandoned requests and transports. Keep ownership from spawn, escalate SIGTERM to SIGKILL, force pending reaps when the sidecar exits, and clean each failed startup's private socket directory. Based on #14494 by @JuuuuHong. Preserve the original helper ownership/reaping design and regression tests while retaining the upstream line-buffer optimization and adding real-process teardown and resource-bound tests. Fixes #9141. Co-authored-by: JuuuuHong <juhang720@gmail.com> |
||
|
|
c33a446190 |
feat(mobile): clarify the notification opt-in screen (#20930)
* feat(mobile): clarify the notification opt-in screen Replace the generic enable-notifications prompt with copy and a looping banner preview that show background alerts when an agent finishes or is waiting, even if the app is closed. * fix(mobile): share reduced-motion hook and wait before animating Extract the duplicated onboarding reduced-motion probe and hold the banner loop until the OS preference is known, so Reduce Motion users do not see the first cycle. |
||
|
|
d4c19d5db4 |
test(mobile): let the RPC recorder open a subscription and script its frames (step 6 capability) (#20920)
* test(mobile): let the RPC recorder open a subscription and script its frames The request-only runner threw on `client.subscribe`, which is why seven raw-port holdouts read "the recording runner refuses to open one". It no longer does. `ScriptedRpcTransport` drops the real `RpcClientStreamRegistry` into each physical session, the way it already reuses `RpcClientRequestTracker` for requests, so subscribe params, frame routing and the unsubscribe wire all come from product code. Per session, not shared: a frame is routed by the session that published its subscribe, and after a cutover the retiring registry is what holds a cancelled subscribe long enough to unsubscribe it once its id arrives. A subscribe writes to `payloads` through the same hook a request does, named by per-method occurrence, and frame ids come from the transport's existing counter because the real `DirectRpcClient` shares one counter across requests and streams. New scenario step kind `frame`: it names a subscribe payload, asserts its params the way `complete` does, and hands a whole host response to the real `handleResponse`, so `ready`, a data event, the host's end-of-stream pair and a refusal are one step kind rather than four. Every `payloads` entry now carries `sent`, the request count at write time, the same stamp `effects` already use. Without it, swapping `client.subscribe` and the first `sendRequest` in a product source moves zero bytes: a subscribe publishes synchronously while a request waits for connected, so the payload order is identical either way and only `sent` moves. The reply matrix now drives frames as sites, named by payload and occurrence because one subscribe carries many frames. Nine of the eleven partitions apply; the two transport rejections are what a request promise fails with and a subscription holds none. Success shapes keep the scripted frame's `streaming` flag, which is what routes a response to the open stream. `useFocusEffect` is substituted as `useEffect`, so a route's focus cleanup is recorded at unmount and a blur-triggered unsubscribe stays unrecorded; the README says so rather than a driven focus substitute no recording reads. Four tests, each killing a named mutation: routing a frame through the current session instead of the publisher, delivering a frame to the request tracker, dropping the `sent` stamp, and reading only `'complete' in step`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the two runtime client-event stream consumers Two families, both driven through the new frame step, as the capability proof for the subscription recorder. `session.live-worktree-name` mounts `use-live-worktree-name.ts` end to end: subscribe, `worktree.show`, a `ready` frame, the fulfilled name, a `worktreesChanged` frame, the follow-up `worktree.show`, then unmount and the `runtime.clientEvents.unsubscribe` its focus cleanup sends. `worktree.host-refresh` mounts `startHostWorktreeRefresh`, whose whole output is when it calls the two fetches it is handed. It sends no request of its own, so it is also the family that would have thrown `No scripted reply to drive a matrix over` before a frame was a matrix site. The 3 s foreground poll is driven by an `advance` step, which puts `WORKTREE_REFRESH_MS` under recorded time. Both adapters live in one new module, registered like every other domain, so the two families' goldens are pinned to a file that holds only them. No product source changes and no call site migrated: the seven raw-port holdouts and the `client.subscribe` zero-reference assertion belong to the migration PRs. `accounts.subscribe` in `use-mobile-home-host-connections.ts` is left out. Its snapshot decoder is re-exported through a React Native screen module the loader cannot reach, which is the same wall the accounts read has always been behind, so it needs a substitute beyond what these two read. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record every golden for the subscription recorder Engine files changed, so `recorderSha256` moves and every header re-digests, and `payloads` entries carry a new `sent` key. Nothing recorded moved. Recorded from a detached worktree at the pinned baseline with this branch's recorder laid over it, per the README's awkward case; `baseline` is unchanged. Decoding both sides through the value pool and ignoring `recorderSha256` and the new `sent` key: 641 compared, 6 header-only (the six goldens with no payload at all), 635 sent-only, 0 other, 9 added, 0 deleted. The 9 added are the two new families: a pilot golden each, four reply-matrix sites for the live title (two requests and two frames) and three for the host refresher (three frames, and no request of its own). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the broad object parameter out of the frame partitions `audit:anti-slop`'s no-object-parameters rule fires on a parameter typed `object`, which the frame-partition helper took to spread a success envelope. One function narrowing `unknown` to a spreadable envelope replaces the two that split the check, and the streaming flag is now read as `=== true` rather than by key presence, matching `isStreamingOpenerReply`. An engine edit moves `recorderSha256`, so every golden re-digests again. Decoded through the value pool, all 650 differ on that header alone and on nothing else. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): refresh the recorder's own scenario and golden counts The paragraph still claimed 78 scenarios and 153 goldens over 210 tests, which went stale across the domain additions since. It is 330 scenarios, 650 goldens and 757 tests as of this branch. The figures quoted further down are measurements of the change each one describes, so they stay as written; a line now says so. Prose is excluded from `recorderSha256`, so this moves no golden. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the inert optional off a frame, and pin the replay re-read Review of #20920 found four things the first pass got wrong. The `optional` flag on a frame step never gated anything: the registry routes every streaming response to the id that opened the stream, retired or not, so `frame()` only ever throws for a non-streaming reply. Dropping the parameter, the step field and the downstream marking moves the scenario digest of two matrix goldens and no recorded byte. The session comment claimed a mechanism that is not there. The re-send after a cutover comes from the logical client's own subscription replay, not from the registry being per-session; a shared registry is byte-identical. What being per-session buys is a frame routed through the session that published its subscribe, which is what `DirectRpcClient` does too. The host-refresh scenario now cuts over and answers a second `ready`, so the reconnect replay branch is recorded: deleting its re-read moves this family. Before, that branch was source no golden reached. README over-claimed the subscribe port as covered. Nine product call sites subscribe, two are recorded, and the other seven are now named with what stops each. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record for the frame flag removal and the replay cutover 644 goldens move on `recorderSha256` alone, from the engine edit. Two more also move `scenarioSha256`: the live-worktree-name matrix variants that used to carry `optional: true` on a downstream frame. Four bodies move, all in `host-worktree-refresh` — the pilot and its three matrix goldens now record the cutover, the re-subscribe payload, the retiring unsubscribe and the extra worktree/repo read the replay branch does. One golden is added, for the matrix site the second subscribe payload opens. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): count the golden the second subscribe payload adds Prose only; moves no golden. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the live-worktree-name replay re-read too The same cutover treatment as host-refresh: the scenario now migrates the logical client, answers a second `ready` on the re-sent subscribe, and answers the title read the replay branch makes. Before this, deleting that re-read from `use-live-worktree-name.ts` moved no golden. No engine file changes, so `recorderSha256` holds and 646 goldens are byte-identical. Five bodies move with their scenario digest, all in this family, and two matrix goldens are added for the sites the second subscribe payload and the third title read open. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say what a request count cannot order, and name the accounts wall `sent` counts requests, so it orders payloads and effects against sends and not against each other. A family that sends none has no ordering at all: `host-worktree-refresh` keeps `sent` at 0 through every checkpoint, and moving its two initial reads across the subscribe moves no golden. The fix is one write ordinal shared by all three lists, which forces a full refresh. The `accounts.subscribe` wall was misdiagnosed. The loader reaches `decodeAccountsSnapshot` and it throws its own domain error; what the runner cannot supply is the multi-host client context `useAllHostClients` reads. Also honest about the record recipe: where a branch must not repin `baseline`, the detached-pin worktree is the only one that runs, merged main or not. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): file only a subscribe as an open stream, and drop three unused seams The registry sends its unsubscribes through the same `sendEncrypted` hook as its subscribes, and the hook filed every payload under `openStreams`. A frame aimed at an unsubscribe name therefore routed at that wire id, matched no stream, recorded nothing and reported success — where the README promises `Missing subscription payload`. A latch around the session's `subscribe` wrapper files only what a subscribe published. Its test fails without the latch. Three seams no caller varies, the same shape as the `optional` flag: `frameReplyPartitions` took a `scripted` reply to copy `streaming` from, but every frame site scripts a streaming reply, so the flag is stamped and a non-streaming unary closer as a base frame is called unsupported; the divergence map's three-deep ternary is early returns, since `index > divergence` already implies `index !== divergence`; and `MatrixSite` is no longer exported. Body-inert: re-recording into a scratch dir at this tree moves all 653 goldens on `recorderSha256` and nothing else, decoded through the value pool. The goldens are left stale for the merge re-record. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record every golden after the main merge One record at the pin, with this branch's recorder, scenarios and driver script overlaid on a fresh detached worktree. Decoded through the value pool against `origin/main`: 667 shared goldens, 6 header-only on `recorderSha256`, 661 also gaining the `sent` stamp this branch puts on every payload entry, nothing else moved, and 12 added — the two client-event families and their matrices. No `adapterSha256` moved, so main's adapter work was already recorded against its own goldens. Those 12 are byte-identical to their pre-merge bodies, `recorderSha256` aside, so the merge changed nothing this branch recorded. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
b8d4cde09f |
refactor(mobile): send six screen-mounted call sites through typed RpcOperations (step 4, wave 3) (#20919)
* test(mobile): record six screen-mounted call sites before migrating them Five new mount adapters and six scenarios, recorded against the pinned baseline's product code so the goldens are main's behaviour, not the refactor's. Each site is a screen the recorder could not previously mount: - `home.host-accounts` mounts `fetchMobileHomeAccounts`, whose decoder is re-exported through `AccountUsage.tsx`. That module loads under the mount loader, so the inventory's "no recording can load it" was already stale. - `notifications.display-test-screen` mounts the settings push probe and presses its button by reading the handler back off the rendered inert `Pressable`. - `aiVault.history-screen` mounts the history panel, which is where the last `worktree.ps` lives. Split in two: the base stops once the worktree list has seeded the scopes, because a reply partition there changes the scopePaths the downstream `aiVault.listSessions` carries, and a matrix variant cannot assert params it moved. The full chain is a second scenario, driven as a pilot only. - `tasks.route-repo-list` mounts the tasks screen-root hook and calls its own `ensureLoaded`, which is the only thing that fires `repo.list`. - `linear.select-workspace-picker` calls the render helper the tasks surface calls and invokes the `onSelect` on the element it returns. The picker draws inside `BottomDrawer`, whose reanimated timing driver and gesture builder the recorder would have to impersonate for a row to exist; the closure is the same either way, and the workspace a selection carries comes from the scenario. Five substitute members are added, each with the recording that reads it: `react-native-safe-area-context.useSafeAreaInsets` and `expo-router.useLocalSearchParams` for `tasks.route-repo-list`, and `react-native.TextInput`, `.SectionList` and `.RefreshControl` for `aiVault.history-screen` once its list renders. `useLocalSearchParams` answers one pinned route for the same reason the window size is pinned: a screen's own address is not a device reading, and the one screen that reads it sends `repo.list`, which takes no params. Touching the substitute table moves `recorderSha256`, so all 641 existing goldens are re-recorded. Recorded from a detached worktree at the pinned baseline with this branch's recorder laid over it: every pre-existing golden is header-only, verified by resolving both sides through the value pool — 641 header-only, 0 body, 0 deleted, one distinct `recorderSha256`, `baseline` and `lockfileSha256` across all of them. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the linear workspace picker's model fixture `mobile/tsconfig.json` covers the recorder, and the fixture's setters were written with the argument the product happens to pass rather than the `SetStateAction` the model declares. Typing them moves `adapterSha256` on the two goldens recorded through this module, so they are re-recorded here rather than in the refactor commit, which must move none. Re-recorded at the pinned baseline: `linear-select-workspace` and its reply matrix, header-only, bodies unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send six screen-mounted call sites through typed RpcOperations Nine references off the raw request port, across six files. Every one is proven against the goldens recorded in the previous commit from the pinned baseline's product code: this commit moves no file under mobile/rpc-foundation/goldens. Reused rather than redefined: - `worktree.ps` in the history panel sends through `worktreeCatalogRead`. Same question, same acceptance — a refused list leaves the screen on what it holds. - `repo.list` in the tasks screen-root hook sends through `newTabRepoListRead`. Its policy raises the host's message and its reader takes `repos` off the payload while preserving the property-read exception a null result used to throw at the cast, which is what this call site did by hand. Its name still says new-tab; a third consumer does not make renaming it this bucket's business. Four operations are new, each because no existing reader on the method takes this consumer's input: - `files.read-directory-or-skip` and `files.legacy-explorer-list-or-skip` for the explorer. Both skip, because neither refusal is the operation's to decide: the readDir refusal code selects the legacy fallback and the list refusal supplies the message. The existing `files.list-or-skip` reads the `files` member alone, and the explorer also needs `truncated` for the "Showing first 5000" note. - `accounts.home-snapshot-or-skip` for the Home card, decoded by `decodeAccountsSnapshot` at the call site as before. - `notifications.test-push-or-skip` for the settings probe, whose `forbidden` and `method_not_found` refusals mean "try the next desktop". - `linear.select-workspace-or-skip` for the filter sheet. Two behaviours are preserved rather than repaired, both recorded: - The workspace switch never read its reply. `.then(() => loadLinearContext())` runs on a refusal exactly as on a success, so only a transport rejection reaches the error copy. Interpreting the operation here would surface a refused switch for the first time; that is a product change with its own re-record. - `app/terminal-settings.tsx` still reads `ms` off the reply envelope instead of off its result, so the value is always undefined. It did not migrate, and the inventory now carries the defect as its own note. Four mutants are added, one per new family that admits a state-only one: the Home snapshot, the push test result and the tasks repo list each decoded one level above the envelope, and the workspace switch with its context reload dropped. `aiVault.history-screen` gets none and says why in the suite: everything `worktree.ps` publishes also moves the `scopePaths` the next scripted completion asserts, so a mutant aborts the sequence instead of diverging from it. Its evidence is the reply matrix at that request. The tasks source-parity ratchet moves with the family it guards: hook, statement, declaration, render and style counts are unchanged, and the semantic source is a pure deletion of four lines — two `rpc:` call signatures and the two method literals they carried. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): matrix the six new screen families' replies One golden per scripted reply, eleven partitions each, recorded at the pinned baseline alongside the pilots. Seven sites: `accounts.list`, `notifications.testPush`, `repo.list`, `linear.selectWorkspace`, and all three of the history screen's — `worktree.ps` and the two `status.get` reads its scan chains off the worktree list. The history matrix is also that family's defect evidence in place of a mutant: every partition at `worktree.ps` changes the `scopePaths` the downstream `aiVault.listSessions` carries, and the sender args are recorded with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): correct three operation and mutant comments Comment-only, no product behaviour and no golden movement. - `worktreeCatalogRead` says two readers; there are three. Names the third (the agent-history panel's `scopePaths` seed) and drops the stale count from the module header, which described call sites rather than the two operations. - `newTabRepoListRead`'s census counted the two operations over `repo.list`, not its own two callers, and claimed both read a workspace's connection id. The tasks route keeps the whole list for its repo pickers. The split from `nativeChatRepoListRead` stays where it belongs: acceptance. - The `aiVault.history-screen` mutant note pointed at the reply matrix as the accepted-vs-refused oracle. Decoding `matrix-aivault.history-screen-worktree.ps-1.json` through the value pool shows `normal`'s projected state is identical to all seven non-crashing partitions (spinner, two labels, zero rows). The real oracles are the next request's `scopePaths` (`["/repo/feature"]` vs `[]`) and the crash channel the three `inner-*` partitions land in. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): give the second files.list reader its real reason Comment-only, no product behaviour and no golden movement. `legacyFileListRead` claimed "the member reader rejects this consumer's input". Nothing rejects: `rpcUncheckedMemberReader` returns the member, and reusing it here would simply drop `truncated`. The reason the explorer declares its own operation is the other direction. Widening `files.list-or-skip` to a payload reader would split the `workspace-files` variant it shares with `nativeChatFileSearchRead` over `files.searchPaths`, whose only caller feeds both through one `extractPaths` in `use-mobile-native-chat-file-search.ts`, so the member read would move into that hook rather than disappear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): indent the six scenario entries spliced during the merge The conflict on `pilot-scenarios.json` was resolved by id rather than by hunk, splicing this branch's six entries into main's text at the array's close. The splice started at the entry's `{` instead of at its line, so those six lines lost their indentation. oxfmt's only change is those six lines; the parsed document is identical, and the recording suite still matches all 667 goldens, so no scenario digest depends on the raw text. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the merged goldens once at the pin One record for the whole merged tree, at the unchanged baseline |
||
|
|
615b1370fb |
refactor(mobile): own the request/cache lifecycle in GenerationScopedRequestOwner, piloted on the legacy file inventory (step 5) (#20914)
* feat(mobile): own the request/cache lifecycle in GenerationScopedRequestOwner (step 5) Hooks guard stale replies with hand-rolled generation counters, `isCurrent` callbacks and latest-wins refs, so the guard is a callback a caller may forget. The owner keeps the cache, the in-flight identity and the generation token private. `read` and `load` are handed the scope and build the key themselves, so a scope the owner has not seen retires everything it held before it answers, and two workspaces cannot share a key. Publication goes only through `commit(lease, value)`: the lease brand is module-private, so no caller can mint one, and a lease whose generation moved is refused. `reset` bumps even when the scope came back to where it started, as in A to B to A. Three epochs may sit in a scope and they are not the same thing: the logical authority epoch, the physical authenticated session and the negotiated capability epoch. Which of them retires a given owner's data is that owner's decision, expressed by what its callers put in the scope. `lifecycle-owner.test.ts` carries one named schedule each for key-reset-cleanup, blur, cutover, reconnect-mid-request and stale-inflight-cleanup, each written as an explicit resolution order. It also fences loader bodies: a `load` callback that writes state it did not declare is rejected by the same kind of source scan that fences raw casts. Compile-time assertions live in a non-test file because mobile's tsconfig excludes tests. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): put the legacy file inventory on the lifecycle owner (step 5) The native-chat file search kept three hand-rolled guards for one request: a generation counter bumped by an effect, a committed-paths ref, and an in-flight ref whose `finally` cleared itself conditionally. The stale-reply check lived in the reply handler, where a caller could forget it. The owner replaces all three. `read` and `load` are handed the scope, so the guard runs before either can answer, and the reply is published only through `commit(lease, value)`. What retires the inventory is named at the call site: this host, this workspace, this logical authority epoch. A reconnect to the same host leaves the files on disk alone, so the physical authenticated-session epoch is deliberately not in the scope. `RpcClient` gains one optional read-only signal, `getGeneration`, so a holder of a bare client can scope cached work to the logical authority epoch that `StableLogicalRpcClient.migrateTo` advances. Nothing else about either client widens. No golden moves: all nine legacy-inventory recordings reproduce byte for byte, including the A-to-B-to-A and cutover schedules. The `race` mutant is re-anchored on the owner's generation compare, which is now the only place that compare exists, and it still dies against b1. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): cut the lifecycle owner down to what callers use (step 5 review) Review round 1 on #20914 found three pieces of surface with no product reader and one vacuous assertion. `dispose()` is gone with the `disposed` field, the three guards that read it and the `'disposed'` verdict arm. A React effect cleanup cannot use it: the pilot's cleanup runs on every dep change and the owner outlives it in a ref, so a workspace select would dispose it permanently. Swapping `reset()` for `dispose()` there fails 7 tests across 3 files. `capacity` and its eviction loop are gone too. No caller varied it, so the loop never ran in production, its `if (oldest.done) break` was unreachable, and it evicted in insertion order while its name said capacity. `RequestCommitVerdict` and `RequestParameters` lose their `export` (no importer), as does the `generation` getter and the expect-error assertion that pinned it (test-only reader; `reset` advancing is proven by the verdict a lease from the previous generation gets). `LoadedRequest` keeps its export: it names the value of the public `load` promise, which a helper over that result has to write down. `key-reset-cleanup` now leaves a second request pending across the `reset()` and asserts the post-reset load starts its own, which is the half `inFlight.clear()` actually owns. Proof: deleting that line from `retire()` failed this schedule and `stale-inflight-cleanup`; before the change it failed only the latter. `read`'s doc now says it retires an unseen scope before answering and must not be called from render. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): read getGeneration off RpcClient and scope one attempt once (step 5 review) Two call-site findings from review round 1 on #20914. `mobile-session-tabs-stream-health.ts` hand-rolled `RpcClient & { getGeneration?: () => number }` and cast through it with no SAFETY rationale. `RpcClient` declares the member now, so both go and the read is `this.options.client.getGeneration?.() ?? 0`. The file-search pilot built its scope from a function it called twice in one attempt, so a `migrateTo` landing between the cache read and the load would have put one attempt in two scopes. It is a `const` computed once per attempt. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): type the scope, drop the in-flight wrapper (step 5 review) Review round 2 on #20914, owner side. `RequestScope`'s element type now excludes symbol and bigint, so both are compile errors with an assertion each in the fence. The runtime symbol throw is gone with the untested branch it guarded, and the bigint case it never covered (it reached `JSON.stringify` and threw V8's serialize message from two frames down) cannot be written. `InFlightRequest<Value>` existed only so its own `then` callbacks could name the entry they belonged to, which forced a throwaway `Promise.resolve(null)` that the next statement overwrote. The map holds the request promise itself and `settle` compares promise identity. `scopeMember` is inlined into `scopeKey`'s map callback: with symbol gone the member type is the scope's element type, which spells `object`, and anti-slop bans that in a parameter position. Inferred in a callback it is the same type with no annotation to ban. Header: `committed` says the generation still holds, not that the value already in the caller's hand is fresh. The pilot displays `loaded.value` directly and is fenced by the sequence counter it had on main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): gate the epoch in the pilot scope and the in-flight slot identity (step 5 review) Review round 2 on #20914 found two invariants no test held. The pilot's scope: replacing `client.getGeneration?.() ?? 0` with `0` left all 711 tests green. The new schedule pairs a control with the claim. A second query under the same epoch is answered from the inventory already held, and a query after the epoch advances issues a second `files.list` and displays what the new authority's host returned. Same client object, same workspace, so the epoch is the only thing that can retire it. Proof: with the literal `0`, `files.list` count is 1 where 2 is asserted. `settle`'s identity guard: making the delete unconditional left all ten schedules green. `stale-settlement-cleanup` puts a request in flight, resets, starts a live request on the same key, then settles the retired one last, whose cleanup names the slot the live request now holds. A third load must join rather than start. Proof: unconditional delete gives `started` 3 against 2. The fake clients go through one `fakeClient` helper, which is what lets the new case name the two members the hook reaches without a fifth `as unknown as RpcClient` (four deleted, one fenced assertion left with its rationale). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
ea7902cbee |
refactor(mobile): send the device-state holdouts through typed RpcOperations (step 4, wave 3) (#20915)
* test(mobile): record the terminal input surface before migrating it (step 4) Three families the recorder could not reach before, recorded against the pinned baseline's product code so the migration that follows has a parity oracle. The device state these hooks read is real, not declared. The pasteboard is the engine's existing per-recording fixture, so a paste reads the bytes a recorded copy put there one action earlier; the buffered draft store is the product's own useBufferedTerminalDrafts mounted in the same tree. No engine file is touched, so no existing golden moves and no header re-digests: 13 new goldens, 641 unchanged. Only the clipboard's text path is driven. The image path decodes a raster through expo-image-manipulator and stages it on expo-file-system, and recording it would mean inventing image and file-system behaviour. Both paths reach the same send. Two family mutants, one per family whose state() can observe a reply: keeping a refused send's draft cleared, and resolving the first repo's connection instead of the workspace's own. The paste family gets none — the hook returns void and calls onSuccess for an accepted and a refused send alike, so its only reply-dependent behaviour is the takeover report, which lives in the sender list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the device-state holdouts through typed RpcOperations (step 4) Ten references over six files, the last of the raw-port sites whose blocker was that a recording could not reach them. Zero goldens move: every one of the six was recorded first, and the suite replays them against the rewritten code. Two operations are new and four sites reuse one that already fixes their method: - accounts.consumeCodexResetCredit, throw-message, payload unread — the call site's decodeResetResult is one scope-and-snapshot check and splitting it across a reader would put one refusal rule in two places. - notifications.getMissedSince, skip — a background pass with no screen to raise a host message on. The member read stays where the optional chaining was. - repo.list: the accessory's connection lookup joins the new-tab reader, which already threw the host's message; the new-workspace dialog joins the skip reader, which already left the list it had. Same reader, same policies, no new acceptance rule and no third operation on that method. - terminal.send: the composed send, the live keystroke send and the clipboard paste all join terminal.input-send, which the accessory raw send already used and which reads acceptance the same way isTerminalSendRpcAccepted did. The typed contract is stricter than the client's own scope type on the redeem: the catalog pairs each runtime with the distro it may name, while the shared CodexResetCreditExpectedScope does not. The invariant is real and held by the attempt journal's schema, so the narrowing is asserted at the send with that named; the bytes are unchanged. Widening the catalog would be a wire change. Two source-shape ratchets pinned the old call text and move with it. The route parity suite's runtime strings drop from 540 to 537: the three method literals that became operation definitions, and nothing else. Every hook, callback identity, effect, JSX and style pin is unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): correct the terminalInputSend and PTY-mode holdout comments `terminalInputSend`'s doc still claimed two call sites. It now has five non-test consumers, all on the same acceptance: the query-reply responder, the live accessory raw send, the session screen's composed draft send and live keystroke send, and the clipboard paste. That comment is where the next person narrowing `object-result-or-null` learns whose lost-ack meaning they are changing, so it names all five and their files. The session inventory block closed with "opens or rides a subscription, or takes its method as a parameter", which no longer covers every holdout below it: `use-mobile-session-terminal-input.ts` is held out for a webview handle. Its own reason also said PTY mode was unavailable in the runner, which this branch's terminal-input adapter contradicts by fixturing the mode map a paste reads. The sentence is narrowed and the holdout restated: PTY mode is recordable, the live webview handle is what is left. Comments only. No product behaviour, no golden, no parity hash moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pair the draft-restore mutant with the refused send `terminal-send-refusal-restores-draft` documents the harm of a refused send that leaves the composed draft cleared, but it was driven by the accepted scenario, where the kill comes from the inverse (a draft restored after a send that landed). The refused scenario shows the documented harm directly: without the restore the input stays empty after the runtime says no. Still one mutant per family, and it kills there — verified by running the suite, `terminal-input-send-refused: kills terminal-send-refusal-restores- draft`. No golden, no product change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): correct the gesture-input holdout and drop a dead repo type Three round-2 corrections, comments and one dead type; no behaviour. The gesture-input holdout claimed a recorder gap that does not exist. The flush path reads refs only — client, connection state, PTY modes, the gesture buckets, active handle and tab type — and the clear-buffer reference optional-chains the webview ref, so a mount with a null terminal ref puts both sends on the wire. The reason now says what is true: those 2 references are migratable as they stand and were out of this PR's bucket. The session summary sentence no longer offers a webview reason. `RuntimeRepoSummary` in mobile-session-route-types.ts lost its last consumer when the accessory hook moved to `MobileRuntimeRepoSummary`; `git grep RuntimeRepoSummary` now finds only the `Mobile`-prefixed type. Deleted. Both refreshed route-parity hashes still credited the `interpretOrThrowRefusalMessage` refresh for their current value. They now state the invariant they pin and this PR's reason for the move: the sends and repo reads inside those bodies name their `RpcOperation` instead of the raw `sendRequest` port. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
7ec2986fd1 |
fix(lint): merge the duplicate agent-status contract type imports (#20907)
main's tip fails audit:code-quality:native on import(no-duplicates), which reds the static analysis and verify jobs of every open PR via the merge ref. |
||
|
|
3520e8eb41 |
fix: highlight bash fences in Markdown source mode (#20592)
* fix: highlight bash fences in Markdown source mode * refactor: trim shell fence alias registration Drop the speculative exports and document the alias-resolution rationale in one WHY comment; the idempotency guard stays. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
a9232e8db6 |
fix(claude): single-own turn identity so Stop reaches a provider-opened turn (#20794)
* fix(claude): single-own turn identity so Stop reaches a provider-opened turn Stop silently failed on any Claude turn the provider opened on its own — a background task reporting in wakes the agent — once the session had dispatched at least once. The transcript read "The provider had already finished this turn." while the model kept working. Turn identity was minted twice from the same stream by two components that never talked. The journal translator writes turnId into the durable turn row, which is the id every client's Stop carries. settleWaiter separately wrote session.activeTurnId, only ever on the dispatch-echo path, and nothing cleared it. Cancel read the adapter's copy; prompt binding, status and both clients read the journal's. They agreed only when a send echo opened the turn. Turn identity is now single-owned. The open turn moves out of the translator's closure into ClaudeOpenTurn, which holds the turn and publishes its lifecycle row, so the id readers ask for is the id the row carries. activeTurnId and activeTurnSequence are deleted rather than widened, so the second writer goes with them instead of a second guard being added beside the first. activeTurnSequence was never turn identity: it asked whether a send was still awaiting its echo, which an interrupt would release as an unexpected turn. That is now derived from the live dispatch waiters. Deriving it also retires a latch — a retired waiter left the stored sequence permanently behind the dispatch sequence, refusing every later Stop for the life of the session. Also fixes the mirror defect the same hazard caused: a stale turn id was accepted against a newer provider-opened turn, because activeTurnId was never cleared when a turn ended. The Claude adapter fixture now acquires with a journal sink, as production does; without one it modelled a session that never ships. * fix(claude): reject stale stop after turn settles * fix(claude): preserve dispatch cancellation fence * test(claude): cover provider-opened stop integration * fix(claude): derive dispatch cancellation fence from journal * fix(claude): honor journal dispatch status before local sends * fix(native-chat): omit absent dispatch observation * fix(claude): release unresolved stop fence after deadline * fix(claude): bound and poll dispatch admission wait * test(claude): cover dispatch admission fast path |
||
|
|
6da72383df |
feat(agent-launch): one executor for agent launches, exposed as agent.launch (#19849)
* refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
* feat(agent-launch): add the launch intent and the one executor that runs it
The sequencing around the launch decision was duplicated per surface, and the
duplicate is where the bug lives. A new worktree was created agent-first, so
its startup terminal WAS the agent and the structured branch below it could
never be reached — every new-worktree launch was a PTY regardless of the user's
default. Orchestration fixed that for itself in #19431; mobile and the CLI
still have it.
`executeAgentLaunch` inverts the order once, for everyone. When the preference
is structured the worktree is created with NO startup agent, the executing host
is then asked whether it can host a session for the workspace that now exists,
and only then is a surface created. The host verdict cannot be hoisted above
creation: `agentSession.createSupport` only answers for a workspace it can
resolve, which is why the decision stays in two halves.
Agent-first creation is deliberately preserved for PTY launches — it is what
sequences the agent's startup command behind the setup runner, so wait-for-setup
comes for free there.
What actually differs per surface is only how a surface is built (an
orchestration worker's session takes a dispatch hold and a mailbox a plain
launch must not take), so that is injected as a factory rather than branched on.
The intent also strips the reserved agent fields from a migrated create payload:
a caller moving off `worktree.create` passes its existing params, and a stale
`startupAgent` in there would re-create the very path this replaces.
Tests assert order and arguments, not just the resulting mode. Reintroducing
agent-first creation reddens 4 of 11.
* feat(agent-launch): expose the launch executor as the agent.launch RPC
Adds `agent.launch` — one host-side method that decides structured-vs-terminal and
creates the surface — wired to the real runtime factories: `createManagedWorktree`
for the workspace, forking on `startupAgent` exactly as the orchestration worker
path does; `createStructuredAgentSessionForWorktree` for a chat session; and
`createTerminal` for a PTY agent. Allowlisted for mobile, which is the surface the
routing gap was reported on.
`worktree.create` is untouched. Its `startupAgent` keeps meaning "spawn a PTY agent"
verbatim, because it answers with `agentTerminalHandle` only on that path: a host
that quietly routed it to a structured session would hand every older client a
response with no handle and no error. All new behaviour sits behind
`agent.launch.v1`, which the host now advertises and a remote client must negotiate,
so a client that does not gets today's behaviour unchanged.
* fix(agent-launch): drop the deleted draft-prompt blocker from the reason map
main removed the draft-prompt blocker in #19681 (a structured session now holds
an unsent draft), so the exhaustive Record no longer typechecks.
* chore(agent-launch): carry a SAFETY rationale on the agent placement cast
The type-assertion gate landed after this branch's base, so the new file's
copy of the worker-start cast is now a changed-code finding.
* chore(agent-launch): carry agent.launch through main's RPC typing and casting gates
The typed-method contract, the generated params catalog and the
`assertionStyle: never` casting scan all landed after this branch's base.
- AGENT_LAUNCH_METHODS kept an `RpcMethod[]` annotation, which widened its
method name to `string` and broke assignability; every sibling infers instead.
- `agent.launch` binds a schema under src/main, so it joins the catalog's
RPC_METHODS_WITHOUT_SHARED_PARAMS and the parity gate's hand-listed twin.
- The now-typed methods make most test casts unnecessary; the few that remain
carry the line-specific SAFETY rationale the casting gate requires.
* docs(agent-launch): stop the receipt-wording comment claiming a migration
The decision was never moved out of orchestration-worker-start-mode; this PR
adds a second copy beside it. Say so, and name the unenforced agreement.
* docs(agent-launch): stop the executor comment claiming a migration that has not happened
The header asserted two things the tree does not support: that every launch
surface routes through the executor, and that the mode decision "already lived"
in `agent-launch-mode`. `agent.launch` is the executor's only consumer, and
`orchestration-worker-start-mode.ts` is byte-identical (blob
|
||
|
|
13ba649c22 |
fix(terminal): let a runtime-created Windows terminal BE the requested shell (#20825)
* fix(terminal): let a runtime-created Windows terminal BE the requested shell
`orca terminal create --environment <windows-host> --command 'cmd.exe'` never
created a cmd terminal. `--command` is text the provider TYPES into whatever
shell it spawned, so the PTY stayed the host's default shell with cmd running
inside it. Captured on `awin`, whose default is Git Bash:
$ orca terminal create --environment awin --command 'cmd.exe' --json
$ orca terminal send --environment awin --terminal term_10656cf7... \
--text exit --enter
$ orca terminal read --environment awin --terminal term_10656cf7... --screen
neil@awin MINGW64 ~/orca/orca ((30f820708f...))
$ cmd.exe
Microsoft Windows [Version 10.0.26200.9445]
C:\Users\neil\orca\orca>exit
neil@awin MINGW64 ~/orca/orca ((30f820708f...))
$
The handle is alive the whole time and `terminal list` shows one healthy
terminal, because the PTY never changed — so the only symptom is that the
caller's terminal is now a shell it never asked for, and every later `send` is
quoted for the wrong one. On `win-lowspec` (default pwsh) the same create lands
cmd inside PowerShell.
Root cause
----------
There are two spawn preflights and they are twins:
- `src/main/ipc/pty/ipc/spawn-preflight.ts` — renderer/IPC spawns, i.e. a
terminal tab opened in the app.
- `src/main/ipc/pty/runtime/spawn-preflight.ts` — runtime spawns: the CLI's
`terminal.create`, headless `orca serve`, and every paired remote
environment.
Only the IPC twin read the caller's requested shell. The runtime twin passed a
literal `requestedShellOverride: undefined`, so a runtime-created terminal on
Windows could only ever be the host default. Everything downstream of that
point — `spawn-options`, the daemon, `resolvePtyShellOverride` in the relay,
`local-pty-launch-plan` — already honoured `shellOverride`; nothing upstream
could supply one.
Change
------
- Thread `shellOverride` through the runtime lane: `RuntimePtySpawnArgs` ->
runtime `spawn-preflight` -> `RuntimePtyController.spawn` ->
`TerminalCreateOptions` -> the `terminal.create` RPC's new `shell` param ->
`orca terminal create --shell`.
- Thread it through the renderer-backed lane too (`createDesktopTerminal` ->
`terminal:requestTabCreate` -> `store.createTab`), so `--shell --focus` is not
silently dropped on a local Windows app.
- An agent launch quotes its startup command for the shell it will actually run
in, so a requested shell now owns the startup-shell family instead of the
global `terminalWindowsShell` setting.
- Lift the relay's `ALLOWED_WINDOWS_SHELL_OVERRIDES` into
`isSupportedWindowsShellOverride` in `src/shared/windows-terminal-shell.ts`
(membership unchanged) so the CLI, the zod param schema, and the relay refuse
the same names. `--shell` therefore cannot carry a path or a command line into
`pty.spawn`; only allowlisted bare shell names pass.
- Gate on `TERMINAL_CREATE_SHELL_SELECTION_RUNTIME_CAPABILITY`. An older host
strips the unknown `shell` param and answers with a healthy terminal running
its default shell — a reply indistinguishable from success — so the CLI
refuses before creating anything rather than creating the wrong shell quietly.
`--shell` stays Windows-only; macOS and Linux hosts spawn the login shell and
the relay drops the value off win32 rather than honouring it half-way. A WSL
project runtime still outranks it, unchanged.
Tests
-----
- `pty-spawn-shell-override-parity.test.ts` pins both preflights against the
exact drift that caused this (verified failing with the fix reverted).
- `createTerminal` passes `shellOverride` to `ptyController.spawn` with no
startup command.
- CLI: sends `shell`, refuses a shell the host cannot spawn, and refuses a host
without the capability — in both refusals without making the round trip.
- Allowlist and `terminal.create` schema accept/refuse cases, including paths
and appended arguments.
* fix(terminal): refuse a requested shell the execution host cannot apply
The first commit made `--shell` reach the spawn, but only a LOCAL win32
execution host applies it: `spawn-options` gates the override on
`process.platform === 'win32' && !args.connectionId`. So `--shell cmd.exe`
against an SSH-routed worktree, or against a macOS/Linux host, still returned a
healthy terminal running that host's default shell — the same
indistinguishable-from-success reply the capability gate exists to prevent, one
layer down.
Refuse instead, before anything spawns. The check sits at the top of
`resolveAgentTerminalCreateOptions`, which every create lane funnels through, so
neither lane has to remember it; the desktop lane additionally refuses a
worktree-less create, which has no execution host to resolve a shell on.
An SSH host's platform and installed shells are not visible to this runtime, and
a POSIX host has no Windows shell to pick. Neither can honour the request, and
saying so is the whole point of the flag.
Docs and the CLI spec now say "refused", not "ignored".
* fix(terminal): refuse a shell that contradicts the project execution runtime
`resolveLocalWindowsTerminalRuntimeOptions` does not merely rank the project's
execution runtime above a per-terminal pick -- it REWRITES the pick, in both
directions, and says nothing:
- a WSL project forces `wsl.exe`, discarding `--shell cmd.exe`;
- a Windows-host project discards a WSL name and falls back to `COMSPEC`
(`getHostShellForProjectRuntime`), so `--shell wsl.exe` spawns cmd. That is
the common case, not an edge: `resolveProjectExecutionRuntime` resolves
`windows-host` for every project that is not WSL, while a repo belonging to no
project honours `wsl.exe` -- so the same flag behaved differently depending on
whether the repo was in a project.
Either rewrite returns a healthy terminal running a shell the caller did not ask
for, which is the failure `--shell` exists to remove.
It also split an agent launch's quoting from the shell that receives it. The
previous commit made the startup-shell family follow the REQUESTED shell, so
`--shell wsl.exe --command codex` on a Windows-host project typed POSIX-quoted
launch args into cmd. Refusing the contradiction removes that case rather than
papering over it.
Refuse instead, alongside the SSH and non-Windows refusals, from the same
`resolveAgentTerminalCreateOptions` seam every create lane funnels through.
Also from review:
- the allowlist test looped the list against itself; spell the members out.
- the runtime spec case claimed to prove the pty's shell when it asserts the
controller received the field; name it for what it checks.
Reported by an adversarial review of the branch.
* fix(terminal): canonicalize --shell and refuse a WSL-path rewrite
Review of the --shell create path turned up two ways the terminal could
still end up being a shell the caller never asked for -- the exact failure
--shell exists to remove.
Bare and mixed-case spellings passed the allowlist but reached consumers
that exact-match the canonical name: resolveWindowsShellStartupFamily
classified `cmd` as the PowerShell family, resolveWindowsShellLaunchArgs
fell through to empty shellArgs (no `chcp 65001`, no OSC 133 bootstrap that
Windows foreground status depends on), and resolveWindowsGitBashShellPath
compares case-sensitively so `Git-Bash` spawned a literal `Git-Bash`.
The allowlist is now one canonical-name map and terminal.create canonicalizes
on parse, so the spawn path only ever sees `.exe` spellings. `pwsh` and
`powershell` stay distinct binaries.
A `\\wsl$\<distro>\...` cwd made the providers force wsl.exe regardless of
the request, and terminalShellOverrideRefusal only inspected the project
runtime -- undefined for a folder workspace with no project. Refuse on the
resolved cwd and the workspace path, judging what the PTY actually gets.
Also: the capability gate reported an unreachable host as too old rather
than unavailable; the SSH CLI shim dropped capabilities from status, so
--shell there blamed the host version instead of naming SSH; and --shell
had no help entry, rendering bare in `orca terminal create --help`. Adding
that entry crossed help.ts's max-lines cap, so the flag table moved to
flag-help-text.ts rather than suppressing the rule.
Adds a behavioural test for the runtime preflight (the one-line fix was
pinned only by a source-text scan), plus coverage for the startup-command
quoting family, the no-workspace refusal, and the WSL-path refusal.
* fix(build): keep tests out of the RPC params catalog bundle
The catalog walk under methods/ already skips *.test.ts, but the contract
directory glob took every .ts. terminal-create-shell-param.test.ts is the
first test to live there, so the bundle pulled vitest into a CJS build and
the generator threw on require(). Same exclusion, same reason.
|
||
|
|
b5a99462bc |
fix(e2e): fabricate runtime-host health through the snapshot that owns it (#20762)
* fix(e2e): seed the runtime host with a snapshot the host cannot overwrite Since #20003 the published snapshot owns runtime-host health, so a bare status seed no longer survives: main's status owner publishes `checking` for this unreachable synthetic host as soon as any runtime RPC touches it, the host reads `connecting`, and the Add Project dialog falls back to Local — so the host-scoped copy the test asserts never renders. Seed a snapshot pinned at the top sequence instead. applyRuntimeHostStatusSnapshot drops any later publication whose sequence is not higher, and setRuntimeEnvironmentStatus no-ops a snapshot-less write once a snapshot exists, so the bare seed could not have worked either way. Ablated: passes with the pinned snapshot, fails without it. * fix(e2e): fabricate paired-web host health through the snapshot that owns it Companion to the onboarding seed fix. Since #20003 the published snapshot owns runtime-host health, so writing `status: null` alone leaves the paired-web client's verified/ready snapshot in place: addRuntimeHost reads snapshot.transport 'ready' before it ever consults status, and the host still renders Connected while the test waits for Disconnected. Rewrite the snapshot coherently instead, pinned at the top sequence so the live status owner cannot restore the host mid-assertion. The disconnected leg uses transport 'unknown' rather than 'disconnected', because a dropped transport is unverifiable and renders as Connecting; only a never-reached host renders Disconnected. remoteControl is nulled because runtimeControlHealth answers 'available' on a ready control socket even with a null status. Ablated with ORCA_E2E_WEB_CLIENT=1 (the whole file is test.skip'd without it, so a run without that flag reports a passing skip): fails without the change, passes with it. * test(e2e): preserve paired runtime status metadata |
||
|
|
0869d997c8 |
refactor(mobile): send the rest of the session domain through typed RpcOperations (step 4) (#20891)
* test(mobile): record the session domain's remaining call sites before migrating them Freezes main's behaviour for what is left of `mobile/src/session/`: the AI Vault resume pair, the clipboard image upload and its two attachment surfaces, the native-chat terminal writes, session tab activation and reconciliation, the terminal-path tap, the structured agent launch, and the session screen's own reads, tab creates/closes, review actions, notes, markdown save and quick commands. 87 scenarios over 22 new families, recorded from the pinned baseline `c6a72169843ececf3a21da370ac50c5c5a4e6462` through a detached worktree, before any product edit. Ten new modules under `adapters/`, so each domain's goldens are pinned by `adapterSha256` and no existing family re-digests. `native-mounting-substitutes.ts` gains `expo-haptics` (inert: every caller is already fire-and-forget), `expo-clipboard` (a per-recording pasteboard cell, because these screens read back what they wrote) and `BackHandler`/`Keyboard`. That is an engine file, so all 509 pre-existing goldens move on `recorderSha256` and on nothing else. Two families cannot be matrixed at their first request: the clipboard upload chain puts the start reply's `uploadId` into the params of every later call, so a partition that answers the start differently changes a downstream assertion rather than a recorded observation. Their base scenarios stop at that first reply instead, and the fallback arm carries the upload family's second site. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the new session mount fixtures against the product model The ten adapter modules added with the session recordings typed several fixtures structurally — loose maps, a local `Terminal`/`Tab` shape, `unknown` for the review screen state — which `pnpm --dir mobile typecheck` rejects: `tsconfig` covers `src/test-support`, so an adapter is checked like product code even though no test file is. Each one now names the product type it stands in for (`MarkdownDocState`, `TerminalRecord`, `MobileSessionTab`, `ReviewScreenState`, `MobileDiffReviewQueueItem`, `DiffComment`) and supplies its members through `mountFixture`. Only one of those changed a recording: a `DiffComment` requires `side`, so the review actions now put it on the wire and the two `review-mark-reviewed-*` goldens carry it. That is the fixture becoming a real subset of the type it claimed, not a behaviour change — no product source moves in this commit, and the parity claim the next one makes is against these bytes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): format the session mount adapter and the recording manifest `pnpm exec oxfmt --check .` from `mobile/` flagged both after the previous commit's type fixes. The adapter is pinned by `adapterSha256`, so the eight goldens recorded through it are re-recorded from the same baseline; the manifest is not pinned by its bytes — `scenarioSha256` canonicalises the parsed scenarios — so no golden moves for it. Header-only either way: no observation changes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the rest of the session domain through typed RpcOperations Thirty files and 58 references leave the raw request port. Every send now names a declared operation with a fixed method, one acceptance policy and one reader, and no call site casts a reply payload: the AI Vault resume pair, the clipboard image upload with both its attachment surfaces, the native-chat terminal writes, session tab activation and reconciliation, the terminal-path tap, the structured agent launch, and the session screen's reads, tab creates/closes, review actions, notes, markdown save, quick commands and file search. Five new operation modules, plus two readers added to existing ones. Three methods get a second reader, each argued where it is declared: `files.resolveTerminalPath` (the tap branches on five members the grant refresh hands back whole), `files.open` (the tap's miss is silent, the Changes screen raises the host's message) and `session.tabs.list` (the send sheet keeps only terminal tabs, which the reveal poller and the reconciliation controller both drop). `git.stage` carries two acceptances for the same reason `repo.list` does: a tapped file raises its refusal, a bulk sweep counts it. No new acceptance policy. The AI Vault resume launch and the review send sheet now share `mobile-review-terminal-operations.ts` with the PR triage launch instead of re-deriving the same create/send pair, and the file-search hook's `extractPaths` moves into the reader it belongs to. One latent behaviour is preserved rather than fixed and wants a ticket: `mobile-session-tab-activation.ts` decides whether to replay an activation with `error instanceof LogicalClientCutoverError`, not with the message-matching `isLogicalClientCutoverError` that exists because "instanceof can miss across bundle copies". Under a second copy of the module the retry silently does not happen, which the recorder reproduces. No golden scripts a cutover for this family, so none records the wrong behaviour. The offender floor in the port ratchet comes down from 50 to 20: it is an anti-vacuity guard on a list this migration is driving to zero, and 39 files still reach the port. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep main's malformed-reply message and re-anchor two mutants Three findings the reply matrices surfaced that the pilot scenarios could not. The clipboard upload's `uploadId` read goes back to the raw result. A success carrying no result throws a destructuring TypeError there, and V8 puts the destructured expression's source text in the message — which the composer shows. Reading the slot off the interpreted payload rewrote that sentence for every user who hits a malformed reply, on three families' `result-absent` and `result-null` partitions. The cast is the one main made, kept for the message alone. `race` and `new-tab-refusal-order` both anchored in text the migration rewrote, so each matched zero sites. Re-anchored at their new homes; the defect each injects is unchanged, and `probe-hole-witness.test.ts` still shows the probe killing the reorder while every pre-probe scenario survives it. `native-chat-send-delivery-unknown` is added as this domain's own mutant: dropping the delivery-unknown arm of a chat send makes an ack lost after the frame was written read as a definite rejection, which invites the user to send the same message twice. A second candidate — swapping `terminal.list` from skip to throw — survives every golden and is not registered: the inventory hook wraps its whole read in `catch { return false }`, so a refusal and a throw leave the same strip. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): matrix every reply the new session families script Thirty-four reply-matrix goldens, one per scripted reply across the twenty-two families the session migration added, each running the eleven partitions in `reply-matrix.ts`. Recorded from the same pinned baseline as the pilot goldens through the detached worktree, so they freeze main's answer to a result-less success, a null result, an inner refusal envelope, a message-less outer refusal and a transport drop — not the migrated code's. They are laid down after the refactor because they are what found its three remaining divergences, each fixed in the previous commit rather than recorded around: the clipboard upload's destructuring message, and two mutation anchors the rewrite left matching zero sites. Two families matrix only their first request. The clipboard upload chain puts the start reply's `uploadId` into every later call's params, so a partition that answers the start differently moves a downstream assertion instead of a recorded observation; their base scenarios stop there, and the upload family's fallback arm carries its second site. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): keep the new session readers inside the operation cast fence `rpc-operation-cast-fence.test.ts` refuses a type assertion anywhere in the region reachable from `rpc-operation-contract.ts`, and four of the new operation modules asserted their own result type. Each of those reads goes back to `rpcUncheckedPayloadReader`/`rpcUncheckedMemberReader`, and the shape the call site expects is named at the call site, which is where every migrated domain already puts it. `extractPaths` and `readQuickCommands` return to their hooks for the same reason — both were only movable by carrying a cast with them. No golden moves: the readers hand back the same values, and all 705 recordings still compare clean. The two frozen source-parity suites over the session route family are refreshed for the migrated text: three hashes, one runtime-string count, and the inventory hook's acceptance check, which now reads `!isCurrent() || !response.accepted`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop three assertions the changed-code casting gate rejects `pnpm run check:code-quality:changed` reports `consistent-type-assertions` separately from oxlint's own pass, and three sites had no rationale: the resume preparation's payload read, which gets the standard SAFETY line, and two adapter refs whose `null as string | null` is just an annotation written the wrong way round. The adapter is pinned by `adapterSha256`, so its thirteen goldens are re-recorded from the baseline — header-only, no observation moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): answer round 1 on the session migration Reuses `RpcOperationSender` for the file-tap client instead of respelling it as `Parameters<typeof fileTapPathResolve.request>[0]` at two sites; the type is the same by construction, and the handlers file no longer imports an operation only to name its first parameter. Names why `worktree.set-review-notes` stays separate from source-control's identical `worktree.set-review-link`, and why the new-tab loader's two preflight reads do not share the task drawer's readers on the same methods. Corrects the `callAgentSession` holdout count: five call sites across two hooks plus one inside the module's own mutation wrapper, not seven callers across five files. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): state what actually gates the native chat paging read The `nativeChat.readSession` send sits in the paging callback, not in the mount effect. What blocks recording it is that the mount effect's `nativeChat.subscribe` is what arms the offset and generation the callback pages against, and the request-only runner refuses to open a subscription. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): answer round 2 on the session migration The tapped-path resolve is now a re-export of the preview screen's `terminalArtifactPathResolve` rather than a second definition: same method, same skip, same whole-payload read, so the only thing the old comment could claim was a difference that was not there. `fileOwnershipRuntimeStatusRead` already re-exports for this reason. `worktree.show` has four readers, not three. The one the notes read is closest to is `fileOwnershipWorktreeRead`, which reads the same member whole, and acceptance is all that separates them: a file mutation throws rather than write to the wrong host, a session screen without its notes shows none and keeps working. `interpretOrThrowRefusalMessage` is generic, so a caller keeps the interpretation's own type, and eight longhand copies of its try/catch across five files now call it. Three more copies wait on a frozen source hash; see the report. Three operations sit in the module matching their direction: the quick-command save with the writes, the review-notes read and the markdown read with the reads. The quick-command reader is shared across that line, which is what keeps the save from adopting `[]` on a payload the parser rejects. The native-chat readability probe imports `MobileRuntimeRepoSummary` instead of redeclaring it, and a stray mutant comment that described the race entry is gone. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): finish the refusal-interpretation helper across the session hooks Twelve try/catch blocks across the session hooks now call the generic interpretOrThrowRefusalMessage instead of rethrowing refusedRpcMessageOrFallback by hand. Each one throws the same message on the same inputs, and the request stays outside the catch, so a transport rejection keeps its delivery-unknown identity. Two frozen parity hashes move for that reason alone: - HEAD_CALLBACK_BODY_SHA256, for the one converted block that sits in a useCallback (use-mobile-session-diff-comments.ts) - HEAD_NESTED_FUNCTION_SHA256, for the three that sit in plain nested functions (use-mobile-session-content-create-actions.ts) Every other parity hash and every count is unchanged: hooks 269, callbacks 77, effects 24, nested functions 12, plus the callback identity, effect, main-hook, hook-binding, content-hook, native registration/removal and timer hashes. Copies that stay longhand, by design: - the action-level try blocks that wrap the request as well as the interpret and surface the failure to the UI, in the diff-review comment, git and send hooks - the two catches that call setActionError or setError and return instead of throwing, in use-mobile-diff-review-interactions.ts and use-quick-commands.ts - the call sites in files this branch does not touch (pr-ai-triage-launch.ts, mobile-diff-review-loaders.ts, github-pr-rpc.ts, github-pr-mutation-outcome.ts) No golden moves: this touches no golden, adapter or recorder engine file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the goldens at the new baseline pin One re-record of all 641 goldens against pin |
||
|
|
0325f1a22e |
feat(agent-status): add the canonical store and child-work contract (#20717)
* feat(agent-status): add canonical shared store contract * fix(agent-status): harden canonical store invariants * fix(agent-status): close canonical store race windows |
||
|
|
52b6851b6e |
fix(worktree-create): prioritize creation Git and defer background preparation (#20722)
* fix(worktree-create): run create git commands at interactive tier, defer pool side jobs, bound queue wait by timeout
Creating a worktree on a busy machine stalled for minutes because the create's
own git competed for the same admission budget as everything else.
- The create path never set an admission tier, so it defaulted to 'status' and
could never use the scheduler's headroom slots. It now tags the option objects
that reach git directly: the add, the post-add listing, the base-ref probes and
the prepared-checkout finalize. The speculative warm-up and the SSH path are
unchanged.
- The prepared-pool re-arm is a full `reset --hard`; it ran mid-create and held a
general slot. `consumePreparedWorktreeCreate` now returns it as a thunk the
create runs after the startup terminal is spawned. Stale-preparation
reclamation (`worktree unlock` / `worktree remove`) drops to 'background'.
- A command's timeout only armed once its child spawned, so a saturated queue
could hold a 1s command indefinitely. Admission now takes the same deadline and
raises GitCommandTimeoutError without spawning; a caller abort still reports as
an abort.
The tier is kept off the `{ wslDistro }` routing objects: several callers test
those for emptiness to decide whether a repo has local git routing at all.
* fix(worktree-create): keep a bounded queue wait from reading as an absent base ref
The admission deadline added in the previous commit made every create-path probe's
15s/120s budget cover the queue wait. The default-base and worktree-base probes answer
`false`/`null` for any failure, so a saturated queue reported a repo that has origin/main
as having no default base and the create refused to start. Both probe families now let
`GitCommandTimeoutError` through, and the branch-name resolution loop, the push-target
configuration and the post-add listing run at the create's interactive tier so they reach
the headroom the rest of the create already uses.
Also: the deferred pool re-arm re-checks the pool inside the thunk, since `startPreparation`
replaces a map entry outright and would strand a prefetch's locked checkout with no owner;
the shared worktree scan keys on the tier so an interactive listing cannot inherit a queued
status scan's wait; and the deadline's microtask hop is gone, along with two fake-timer
`vi.waitFor` calls that jumped the clock past a 10ms budget before the grant settled.
* fix(worktree-create): preserve probe fallbacks and defer runtime replenishment
* fix(worktree-create): preserve interactive priority through prepared claims
* fix(worktree-create): prioritize CLI creation and preserve SHA probe timeouts
* fix(worktree-create): scope Git execution policy at creation boundaries
* fix(worktree-create): preserve inconclusive Git probe timeouts
* test(runtime): align creation fixtures with scoped Git execution
* test(native-chat): extract windowing layout fixture to satisfy file limit
* fix(git): restore execution-only timeouts while queued
* refactor(worktree-create): remove unrelated error-handling changes
* chore: narrow review scope and clarify preparation timing
* test(native-chat): restore fixture extraction to fix CI lint
* refactor(git): keep the admission scheduler in its original module
Reverts a move-only extraction. Inlines the single-use command-class
wrapper so the tier-resolution import fits the file's line budget.
* fix(worktrees): re-arm the prepared pool after CLI create launches terminals
The runtime create fired the pool re-arm right after materialization, so its
`reset --hard` competed with the startup agent's first git reads. Return the
thunk to the caller and fire it last, matching the desktop path.
* fix(worktrees): skip a preparation whose checkout is still running
An interactive create that claimed an in-flight preparation awaited a checkout
queued at background, so on a saturated budget it yielded to every arriving
status poller until aging promoted it. The create now misses with not_ready and
does its own add at interactive; the preparation stays armed for the next one.
Also drops the one-field policy object from the Git operation executor.
* fix(worktrees): report repo_mismatch before not_ready when selecting a preparation
The readiness filter ran before the same-repo check, so another repo's
in-flight preparation was labeled not_ready instead of repo_mismatch, hiding
the cap-thrash signal for multi-project users. The hit/miss decision is
unchanged.
* test(runtime): type the worktree-meta stub against WorktreeMeta
Main now rejects bare object parameters, and the merge picked that rule up.
* fix(worktrees): wait on in-flight preparations and re-arm the pool on failed creates
A create landing mid-checkout now claims the in-flight preparation and awaits it, as main
did. The `checkoutFinished` filter and its `not_ready` miss reason made the create skip a
prepared checkout that was seconds from done and pay a full cold add instead; on a 40k-file
repo that turned a 0.2-1.5s create into 2.4-4.3s. The preparation's own git also runs at
`status` again rather than `background`, so awaiting it does not park behind status pollers.
Only the stale reclaim stays `background`, which no create waits on.
The deferred pool re-arm now fires on every path, not just the success path. Main armed the
replacement synchronously inside the consume, so a later failure in include copy, push-target
setup, or terminal startup still left one warming. The thunk stays deferred until after
terminal startup for admission ordering, but a `finally` on the desktop create and matching
failure-path fires on the runtime create restore that guarantee. It fires exactly once.
* refactor(runtime): carry the pool re-arm in one holder
The runtime create used three mechanisms to guarantee the deferred pool re-arm fires: a
catch in the git create, a catch on materialization, and a holder fired in the managed
create's finally. The desktop create already used one holder for the same guarantee.
The holder now threads down through the create args, so the git create arms it at the point
it consumes a prepared checkout and nothing below has to handle the failure case. The thunk
already re-checks the pool before arming, so a single fire point in the outermost finally
covers every failure after the consume. Behavior is unchanged; both flipped failure-path
tests still assert exactly one fire, and each fails without the production change.
|
||
|
|
8edec28a55 |
fix(worktree): keep a WSL checkout case so delete cannot take the twin branch (#20273)
* fix(worktree): let a POSIX path keep its case on a Windows desktop `canonicalWorktreePath` folded case whenever `process.platform` was win32, without asking what the path itself was. A WSL or SSH checkout is spelled `/home/alice/ws/feature` on a Windows desktop too, and ext4 is case-sensitive, so `/home/alice/ws/Feature` and `/home/alice/ws/feature` — two real checkouts on two real branches — collapsed into one row. `removeWorktree` picks the row it is about to remove with that comparison and reads the branch off it. Requesting `/home/alice/ws/feature` removed the right directory (the path rides in argv) and then ran `git branch -d -- Feature`. The same wrong row feeds `assertWorktreeUnlockedForRemoval`, so a locked twin blocks an unlocked delete and an unlocked twin lets a locked one through. Whose filesystem a path names is a property of the path, not of the desktop reading it, so a POSIX-absolute path now takes POSIX rules at any platform and a POSIX/Windows pair is never equal — `win32.resolve` would otherwise give the POSIX path a drive root and manufacture the equality. Windows drive and UNC paths, including WSL UNC aliases, keep folding case as before. Two call sites already carried private copies of this rule (`isSameCommonDirPath`, `ipc/worktree-path-comparison`); this is the same rule at the source. The removal path is the one that never got one. * fix(worktree): keep a WSL checkout's case through the UNC spelling too The first commit gave POSIX-absolute paths POSIX case rules, which is right but does not reach the WSL case it claimed. `listWorktreesStrict` runs every listed path through `translateWorktreePath`, so git-in-the-distro's `/home/alice/ws/Feature` arrives as `\\wsl.localhost\Ubuntu\home\alice\ws\Feature` and the POSIX branch never sees it. The removal suite mocks `translateWslOutputPaths` to identity, which is why the end-to-end test passed without exercising the translation production always applies. Driving the real translator, the original defect survived unchanged: a request naming `...\ws\feature` ran `git worktree remove --force ...\ws\feature` and then `git branch -d -- Feature`. The filesystem behind `\\wsl.localhost` is ext4, so the UNC spelling is case-sensitive for the same reason the Linux spelling is — except where Windows genuinely folds: the `\\wsl$` share alias, the distro name, and a drvfs `/mnt/<letter>` tail, which really is a Windows volume. `foldWslUncPathCaseInsensitiveParts` already draws exactly that line and `git-fetch-head-lock` already depends on it, so this reuses it rather than writing a fourth copy of the rule. Windows drive paths keep folding whole. The end-to-end case now drives the real translator instead of the mock, so the translation cannot go missing again without the test noticing. |
||
|
|
946dacc65d |
fix(worktrees): route unstamped local worktrees local in the two states #16841 still fails closed (#16829)
* test(worktrees): cover local worktree owner routing with saved runtimes (#16733) A local git worktree whose rows carry no host stamp fails every owner-routed operation closed as soon as any runtime environment is saved, however unrelated. resolveWorktreeOperationRouteResult establishes positive identity from the worktree/repo catalogs, then discards it: with no runtime active the only exit is the legacy-local gate, which demands an empty saved-runtime list. One saved environment makes that false and the call returns { kind: 'missing' }. These tests state the contract before the fix, so the claim that the fix is purely additive can be checked rather than asserted. Committed red on purpose. Observed at |
||
|
|
72a8c096a3 |
fix(ssh): corroborate an empty lsof answer before calling an endpoint free (#20585)
#18304 decided enumerability after `lsof` runs, keying on stderr, non-numeric output, and an abnormal exit. One failure carries none of those signals: probing as a uid that does not own the socket's holder, `lsof -t -a -U <path>` exits 1 with no stdout and no stderr. Measured on Debian 12 against #18304's own probe, a live relay owned by root probed as `nobody`: probe uid path marker pids merged nobody held.sock lsof [] <- live relay holds it merged nobody stale.sock lsof [] <- genuinely nobody merged root held.sock lsof [10] merged root stale.sock lsof [] The first two rows are byte-identical, so nothing about lsof's answer can separate them. The first reaches `verdict: exited / evidence: no-holder`, which `classifySupersededRelay` maps to `stale-endpoint-removed` and `rm -f` on an inode a live relay is still holding. `hidepid=2` produces the same shape. A positive control does not solve this. Controlling on something the probe itself holds passes precisely when we are blind: as `nobody`, `lsof -t -p $$` returns a pid while the socket query returns nothing. Blindness is to *other* uids, and another uid's process is not ours to manufacture. /proc/net/unix is. It is world-readable and lists every bound unix socket regardless of owner, so an entry for the path alongside no reported pid proves lsof was blind rather than that the path is free. Only an otherwise-clean empty answer is corroborated; a reported pid still stands on its own, and the check is skipped when the answer was already unavailable. Same run, with this change: fixed nobody held.sock unavailable [] <- no longer reapable fixed nobody stale.sock lsof [] <- still reapable fixed root held.sock lsof [10] <- unchanged fixed root stale.sock lsof [] <- unchanged The marker can only ever move from `lsof` toward `unavailable`, so this never authorises an unlink that #18304 refuses. Off Linux there is no /proc/net/unix, the check returns false, and behaviour is exactly as before -- deliberately, because defaulting to `unavailable` there would stop every macOS host from reaping a stale endpoint and trade a rare destructive bug for a universal accumulation one. The tests are Linux-gated for the same reason, with an assertion that the evidence they depend on is actually present so the block cannot pass vacuously. |
||
|
|
981a4821da |
fix(cli,relay): stop reading an unsignalable pid as a dead one (+ unverifiable-collapse sweep result) (#20098)
* fix(cli): stop reporting an unsignalable Orca pid as a stale bootstrap `orca status` falls back to a `kill(pid, 0)` probe when `status.get` cannot be reached, and a bare catch read every refusal as absence. EPERM means the pid exists under another uid -- an Orca reached via ORCA_USER_DATA_PATH, or one started with sudo -- so a live app was reported `running: false`, `pid: null`, `runtime.state: stale_bootstrap`, `graph.state: not_running`. Only ESRCH proves the pid is gone, which is the rule every other liveness probe in the repo already applies (`isProcessAlive` in relay/pty-shell-utils.ts, pack-refs-lock-ownership.ts, runtime-metadata-ownership-watch.ts, and agent-session-process-identity-probe.ts). See docs/reference/ssh-execution-boundary.md. * fix(relay): keep a revived pane whose pid only refuses the liveness probe `revive` gated each serialized pane on a hand-rolled `process.kill(pid, 0)` in a bare try/catch, so any refusal retired the pane. EPERM means the process exists under another uid; only ESRCH is evidence of absence. The file already imports `isProcessAlive`, whose ESRCH-only contract `reapPtyProvenExited` documents 450 lines earlier -- this call site just did not use it. Reuse it rather than keeping a second implementation of the same concept. Malformed pids still skip, as before. See docs/reference/ssh-execution-boundary.md. * fix(lint): clear the casting gate on the pid-probe changes main tightened typescript/consistent-type-assertions to assertionStyle: never, which the rebase brings onto these added lines. The CLI probe narrows instead of casting; the relay test keeps the file's serialize idiom behind a SAFETY-annotated suppression. |
||
|
|
2fccacadbe |
test(mobile): mount screens, declared device stores and declared OS state in the RPC recorder (#20884)
* test(mobile): mount screens, declared device stores and declared OS state in the RPC recorder Three recorder capabilities, each with a test, plus four holdout sites recorded against main's product code to prove them. No product source changes. - JSX compiles through the automatic runtime, which is what product sources use; the classic `React.createElement` emit threw `React is not defined` on the first render of every screen. - An unlisted package answers `__esModule` as undefined, so a default import loads and the refusal defers to the first real member read instead of killing the module at load. - The substitutes table gains the inert view packages a screen needs, split into `screen-native-substitutes.ts` behind the same rule: only what a screen reads is listed. - A scenario may declare `deviceStore` and `deviceState.notificationTray`. Reads resolve the declaration or null and never a write; writes are recorded as effects. Undeclared is unchanged. - `screenMount` mounts a component with a crash boundary, so a reply partition that takes a screen down is a recording rather than a suite failure. Every pre-existing golden re-records byte-identical except `recorderSha256`, recorded from a detached worktree at the pinned baseline. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): declare the crash boundary's state type instead of asserting it The changed-code casting gate counts `null as string | null` as a type assertion. Re-records every golden from the pinned baseline, because the edit moves `recorderSha256`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refuse an unlisted native package by the member a mount reads Both emitted interop helpers short-circuit on `__esModule`, so answering `true` hands the refusing trap back unwrapped to `__importDefault` and `__importStar`. All three import forms now load the importer and throw the named refusal at the first member read, instead of a namespace import silently yielding `undefined` and failing later at the call. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): run a task deferred past interactions instead of dropping it An inert `runAfterInteractions` swallows whatever send the screen deferred, and the recording then claims the screen sends nothing. It runs the task on a microtask and returns the RN-shaped handle, so a cancel before the task runs still prevents it. `Alert.alert` stays inert. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record every golden for the two recorder engine changes Only `recorderSha256` moves, from c58067de to a59b30e7. Recorded from a detached worktree at the pinned baseline |
||
|
|
005616171d |
test(mobile): record the real worktree catalog snapshot result in the RPC recorder (#20873)
* test(rpc-recording): record the real worktree catalog snapshot result `worktree.catalog-snapshot`'s action returned `WorktreeCatalogSnapshotClient.fetch`'s raw result. That value nests the live `RpcClient` under `pending.client`, so `captureValue` threw `Unsupported observation: function`: both goldens baked an `unhandled-rejection` effect and left the action's settlement `pending`, proving nothing about what the fetch returns. Project the result through `projectObservable` on the settlement path, the same way `state()` already shows it and the same way #20667 fixed `transport.pairing-race`. Rejections still propagate unchanged, so the two transport-rejection matrix partitions keep their recorded errors. The matrix golden now discriminates all eleven reply partitions: a full admission, six invalid ones, three `request_failed` codes and two rejections. `recorderSha256` does not move; `adapterSha256` moves on the six goldens mounted through this adapter module, four of which have no other change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(rpc-recording): fail the suite when a golden records a recorder failure A projection refusal settles as data, not as a failed run: `captureValue` throws inside the action, the recorder captures it as an `unhandled-rejection` effect, and `--record` writes a green golden whose action never settled. The class has landed twice — `transport.pairing-race` in #20667 and `worktree.catalog-snapshot` in the previous commit — and reverting either one plus a re-record would go green with the broken golden back. Read every golden and fail on an `unhandled-rejection` effect or on either of `captureValue`'s refusal texts in any observation field. A positive control in the same case asserts both detectors fire, so the absence claim is load-bearing rather than vacuous. Not a recording driver, so `recorderSha256` excludes it and no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(rpc-recording): ban only the recorder's own refusals, not product bugs The gate also failed on any effect named `unhandled-rejection`, which bans a real observation: detached-rejection capture exists to pin a main bug in a golden, and `unhandled-recording.test.ts` pins the capture precisely because no golden carries one today. Banning the name would make recording a genuine product failure a test failure. Drop that detector. `refusalText` alone catches all seven checkpoints of the worktree-catalog regression, because the refusal is the message of the captured error rather than the effect's name. The positive control now seeds that shape — the refusal inside a recorded error under `effects` — so the surviving detector is still proven to fire. Vacuity guards on golden and checkpoint counts are unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(rpc-recording): re-record the worktree catalog goldens over main's engine digest #20874 re-digested `recorderSha256` on all 509 goldens, so the six goldens mounted through the worktree catalog adapter had to be recorded again from the pin rather than merged. Bodies and `adapterSha256` are unchanged from the pre-merge recording. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
1457d3966c |
fix(native-chat): release sessions after provider root exit (#20502)
* fix(native-chat): bound structured chat launch * Fix post-merge test hygiene * Make structured fallback settlement exhaustive * fix(native-chat): release sessions after root exit * chore(i18n): remove legacy fallback copy * test(native-chat): remove terminal fallback census * docs(native-chat): clarify root-exit lease proof * chore(native-chat): drop unrelated formatting * fix native chat launch visibility * test(native-chat): split message rail windowing coverage * fix(native-chat): keep transport gating render-pure * fix(native-chat): coordinate launch prompt settlement * test(native-chat): align unified close ownership * fix(native-chat): correct lifecycle imports and test typing * fix(native-chat): fence restored launch cancellations * fix(native-chat): fence authoritative cancellation snapshots |
||
|
|
783d8feabb |
fix(lint): merge duplicate type imports in the mobile RPC recorder adapters (#20895)
* fix(lint): merge duplicate type imports in the mobile RPC recorder adapters The native code-quality audit rejects a module imported twice in one file, so main's static-analysis job is red for every open PR. * test(mobile): re-record RPC goldens against the merged adapters The duplicate-import fix changed two mount adapters, so the nine goldens that pin them by adapterSha256 needed re-recording. The recorder fence requires the pinned baseline to match the product tree, so the baseline moves to current main, which rewrites that header in all 509 goldens. Every recording body is identical, which also shows the commits between the two baselines changed no observed behavior. |
||
|
|
e7206f62a8 |
fix(mobile): retire a structured operation id the host has refused (#20868)
`agentSession.cancel` kept its client operation id whenever the outcome came back unknown. One of those unknowns is not transport doubt: when the host answers `agent_session_operation_unknown` it has decided about that id and will not run it again, because cancel's mutation plan recovers no unknown ledger row. Every later Stop on that turn re-sent the same refused id, so Stop stayed unusable until the row expired. The RPC layer collapsed both cases into a bare `unknown`, discarding the difference between "the effect is in doubt" and "the host answered about this id". It now reports the second case, and cancel spends the id there while still replaying under genuine transport doubt. `agentSession.conversationCommand` deliberately keeps its id: its plan sets `recoverUnknownFromDurableState`, so a reused id can still replay or rerun. |
||
|
|
22ca862f76 |
test(native-chat): widen real-timer waitFor budget in agent-session-wire handoff tests (#20880)
vi.waitFor defaults to a 1000ms/50ms real-clock budget on this suite (no useFakeTimers), which is occasionally too tight for host.requestHandoff / handoffStatus to settle under a loaded CI shard. Production behaviour is unchanged; the assertions are correct, just sometimes slow to observe. vi.waitFor's own poll loop always runs on the real clock (vitest resolves its interval/timeout via getSafeTimers, which bypasses vi's faked globals), so the lease-renewer test carries the same real-wall-clock exposure despite calling vi.useFakeTimers() for the simulated renewal interval. 5000ms follows existing repo precedent for explicit vi.waitFor timeouts on real-timer waits (e.g. ssh-relay-session-rejected-delivery.test.ts, daemon/client.test.ts, pty-subprocess-io-failure-native.test.ts, windows-msys-job.win32.test.ts), which range 1500-15000ms. |
||
|
|
6c03bb6e82 |
fix(lint): replace Reflect.get with typed property access in mounting substitutes (#20874)
* fix(lint): replace Reflect.get with typed property access in native mounting substitutes main's tip fails `pnpm run audit:anti-slop` (the `static analysis` CI gate) on `no-reflect-get` in mobile/src/test-support/rpc-recording/native-mounting-substitutes.ts, blocking every open PR. The Proxy get trap's key is `string | symbol`; branch on that to keep typed bracket access for strings and a symbol-indexed cast for symbols, preserving the existing throw-on-unsubstituted-member behavior exactly. * test(rpc-recording): re-record goldens for the recorderSha256 shift native-mounting-substitutes.ts changed bytes, so recorderSha256 (which pins every non-adapter file under this directory into every golden's header) moved. Re-recorded all 509 goldens; only recorderSha256 differs in any of them, confirming the checkpoint content is unchanged. |
||
|
|
60d793956a |
fix(native-chat): replace the raw question tool row with an awaiting-input row (#20724)
* fix(native-chat): replace the raw question tool row with an awaiting-input row
A question tool call rendered as ordinary tool activity — "Running
AskUserQuestion" with a clipped JSON payload while live, then a "1x
AskUserQuestion {...}" run header once settled — so the one row the reader
actually has to act on read as machine output.
It now draws as "Awaiting user input: <question>", led by a comment-bubble
glyph, with the label pulsing while the answer is outstanding and reading
"Asked: <question>" once it lands. A grouped prompt names how many questions
it asks rather than quoting only the first, since one row stands for the whole
prompt. Question calls also leave the run header, so the count beside them
reports only the work that actually ran.
Codex journals only the question and never a call for it, and a pending
question was dropped from the transcript entirely — its chat log said nothing
while the agent sat blocked on the reader. Pending questions now project the
same row. Claude journals both the call and the question it raised, so the
call itself is suppressed and the one row is fed from one source.
* refactor(native-chat): derive the awaiting-input row from the question item
The first pass fabricated a synthetic `request_user_input` tool call inside the
shared journal projection so that one renderer could serve every lane. That made
a presentation choice on behalf of every consumer of that projection, including
archives and older RPC clients that never asked for it.
Question presentation is now client-local. The shared projection is restored
untouched, and the desktop transcript derives its own rows: a pending question
keeps a stable identity row through tool folding while its receipt draws the
awaiting line, and the duplicate AskUserQuestion call Claude journals beside the
question it raised is suppressed only when a matching question is open in the
same turn — so an unmatched call, or one from a lane that journals no question,
still reports itself.
Question calls now leave the run together with their paired result, which stops a
summarized ask from stranding its answer as an orphan Result row. A failed ask
keeps its error instead of being folded into the awaiting row, and an ask no
longer contends with a concurrently running tool for the active slot: both are
reported.
Adjacent pending questions — the shape Codex journals, one item per question —
group into a single awaiting row that narrows as each one is answered.
Also ships the three awaiting-row strings in the runtime-required English
catalog. Their call-site fallbacks are a shared constant rather than string
literals, so i18next cannot rebuild them from the call site and they have to be
present for the static-analysis gate to pass.
* fix(native-chat): preserve unmatched duplicate question calls
* fix(native-chat): avoid repeated grouped question text
* fix(native-chat): keep pending question text specific
* fix(native-chat): avoid repeating single question answers
* fix(native-chat): narrow question receipt subject
* fix(native-chat): preserve settled ask calls
* fix(native-chat): cover bridge ask rows
* fix(native-chat): fold settled ask receipts
* test(native-chat): cover settled ask receipt folding
|
||
|
|
22857cd8a0 |
fix(crash-reporting): stop periodic emitters from evicting the crash trail (#20639)
* fix(crash-reporting): stop a once-a-minute sampler from evicting the crash trail The breadcrumb ring is 30 entries and evicts oldest-first, so any emitter that repeats outlasts the whole lifecycle trail. Across 293 field reports three periodic emitters hold 77% of every slot ever shipped and 39% of reports arrive with no lifecycle crumb at all — the "Recent activity" section cannot say what the app was doing. Charge the overflow to the most crowded name instead of the oldest event, so a series is thinned from its oldest end and singletons survive. No allowlist, so a new periodic emitter cannot reopen the hole. * test(crash-reporting): pin coalesced-burst accounting under mid-ring eviction * fix(crash-reporting): scope eviction per origin and spare live coalescing owners Round-1 review found two ways the name-only policy was worse than plain FIFO: - Counting ignored `origin` while the snapshot filters by it, so a busy popout's samples made the main window's singleton look redundant and deleted it. - Names like `renderer_error` carry many independent coalesce keys, so the name became "crowded" out of genuinely distinct errors — and the entry taken was the oldest, i.e. a key still accumulating `suppressedSinceLast`. A crash report is the last snapshot, so an orphaned owner is never re-claimed and the burst count simply vanished. Group by (name, origin), skip an entry a coalesce key still owns unless every candidate is owned, and never consider the crumb that just arrived — its coalesce state is linked after the push, so it would always look unowned. * fix(crash-reporting): trim the report window by the same policy as eviction Round 2 found the fix defeating itself. Fair-share eviction parks one-off crumbs at the ring's HEAD and the repeating series at its tail — and the snapshot then took a plain tail slice of `MAX_BREADCRUMBS - retained.length`, trimming exactly what eviction had just protected. Measured on the previous commit: one retained `renderer_memory_highwater` cost one lifecycle crumb, and three erased the lifecycle trail from the report entirely. That lane fills under the same memory pressure that produces the `renderer_memory` flood, so the two cancelled out precisely when the trail matters most. Trim with `evictionIndex` instead, and route `isCoalescedCrumbStillInEvidence` through the same window — a predicate that disagrees with the snapshot would drop an owner's handle and lose the burst count from the crumb the reader sees. Also strengthens the uncoalesced-burst test, whose only remaining delta against its coalesced twin was the slot count: it now asserts the pane population is absent on the uncoalesced side, which is the signal coalescing exists to keep. --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
2eb93206c8 |
refactor(agent-launch): make the launch-mode decision surface-neutral (#19848)
* refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
* fix(agent-launch): drop the deleted draft-prompt blocker from the reason map
main removed the draft-prompt blocker in #19681 (a structured session now holds
an unsent draft), so the exhaustive Record no longer typechecks.
* chore(agent-launch): carry a SAFETY rationale on the agent placement cast
The type-assertion gate landed after this branch's base, so the new file's
copy of the worker-start cast is now a changed-code finding.
* docs(agent-launch): stop the receipt-wording comment claiming a migration
The decision was never moved out of orchestration-worker-start-mode; this PR
adds a second copy beside it. Say so, and name the unenforced agreement.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
62c5037cc3 |
fix(lint): avoid reflective status entry reads (#20872)
* fix(relay): resolve packaged node-pty from resources * fix(lint): avoid reflective status entry reads |
||
|
|
d130347993 |
refactor(mobile): send the dictation, terminal, notification and browser domains through typed RpcOperations (#20702)
* refactor(mobile): pin each RPC golden to its own mount adapter, not every domain's `recorderSha256` covered the whole recorder directory, mount adapters included, so a domain PR that adds its adapter module moved the header of all 153 goldens. #20568 did exactly that and its merge with main conflicted on that one line in 153 files; every future domain PR would collide with every other in flight the same way. Split the directory at a real seam instead of a filename convention: `adapters/` holds one module per domain, registered in `adapters/mounted-operation-modules.ts`, and `recorderSha256` now covers the engine only. A new `adapterSha256` covers the source of the module that mounts each operation a golden's scenarios drive, read off the same `mounts` calls that build the table the recording runs against, so the pin cannot name a file the runner did not use. Adding a domain's module now re-digests nothing already recorded; editing one fails exactly the goldens mounted through it. `adapter-seam.test.ts` keeps the split from drifting: an engine file inside `adapters/`, an adapter defined in an engine file, a register entry naming the wrong file, and an adapter importing a sibling each fail. The five adapters that were inline in `pilot-mount-adapters.ts` move into their own modules, which leaves that file as the registry and nothing else. `GOLDEN_FORMAT_VERSION` goes to 5 for the new header field; the goldens re-record in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the split recorder/adapter digest Header-only. Every changed line is `recorderSha256` (the engine digest no longer covers `adapters/`), the new `adapterSha256`, or `goldenFormatVersion` 4 -> 5; `baseline` is unchanged and recording ran against the same pinned product tree. git diff -U0 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256|goldenFormatVersion)":' | wc -l 0 The seven `adapterSha256` values partition the 153 goldens by the module each was recorded through: 58 settings, 37 hosted review, 21 source control, 11 new-tab agents, 9 file inventory, 9 tasks, 8 workspace settings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): stop pinning goldens to recorder inputs no recording can read The adapter split left three per-domain edits still moving all 153 headers: the mutant table, the per-family mutant registry beside it, and the probe-hole witness. None can change a recording -- the loader consults a mutant only when a mutant test asks for one, and no suite but the two recording drivers writes a golden -- so pinning them claimed a provenance the goldens do not have and charged every domain a full re-record for it. `mutants/` now holds the table, the registry, the reference states, the mutant suites and the probe-hole witness, and `recorderSha256` skips it. What makes that sound is that no recording can reach it: `operationModuleLoader` takes a resolved mutation spec instead of importing a table by name, so nothing on the recording path names `mutants/` at all. `mutants/mutant-seam.test.ts` checks exactly that, and fails if an engine file names the directory or anything outside imports from it. `recorderSha256` also pins only the suites in `recording-drivers.ts`, which `scripts/rpc-recording.mts` records from, so the two cannot drift. A suite that reads goldens, or writes one to a scratch directory, is no longer provenance for a recorded file. `OPERATION_EXPOSURES` went the other way, because it does change what a recording loads: withhold the resume-metadata exposure and exactly four goldens fail. Each domain module now declares its own exposures and gets its own loader, so `adapterSha256` pins the ones that reached each golden. Two assertions in the digest boundary test were vacuous: `join(root, '.')` normalises back to `root` and hit `recorderSha256`'s per-root cache, so the prose-is-ignored claim never recomputed anything. Each call now spells the root differently. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the mutant and driver exclusions Header-only, and no format bump: the header shape is unchanged. `recorderSha256` moves on all 153 because the engine set shrank, and `adapterSha256` moves on the 58 settings goldens because that module now carries its own exposure declaration. git diff -U0 HEAD~1 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256)":' | wc -l 0 Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): restore the preferences actions the merge resolution dropped #20568 added `resume` and `trust` actions to the `settings.task-preferences` adapter while it still lived in `pilot-mount-adapters.ts`. This branch had already moved that adapter into `adapters/task-mount-adapters.ts`, so resolving the `pilot-mount-adapters.ts` conflict in favour of the registry merge silently discarded them and `tw-task-preferences-resume-write` failed to record at all ("Missing or completed request: ui.set#1"). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens at main's tip after the merge All 208 goldens, header-only. `baseline` moves from |
||
|
|
2dfdbc8657 |
refactor(mobile): send the task provider, detail and board domains through typed RpcOperations (#20685)
* test(mobile): record main's task provider item, detail and board RPC behaviour 35 scenarios over 22 of the 25 files left in src/tasks/, recorded from main so the step-4 migration of the provider half has a frozen answer to compare against. Every one of the 70 references this branch will migrate reaches a recorded wire here, which is the check the workspace-creation half added after it lost three sites to fixtures that short-circuited before the call. Scenario params are observed, not written: a generator drove each adapter with nothing answered, read the projected sender calls back, and emitted the completion steps from them, so no `params` in the manifest is a guess about what the screen sends. Five adapter modules, split the way the screens are: one item's reads, the list and composer, the item mutations, the board's reads and the board's row mutations. `mountModelHook` holds the mount/dispatch/project boilerplate these twenty-two hooks share, so each adapter is only its fixture, its actions and its projection. Two fixture modules hold the task items and the project rows, shared so the same pull request looks the same to the comment hook, the merge hook and the checks hook — which is what makes their recordings comparable. `baseline` moves from |
||
|
|
98784820d8 |
refactor(mobile): send the transport pairing and status domain through typed RpcOperations (#20667)
* test(mobile): record the transport pairing and status domain against main Adds seven recording families for `mobile/src/transport/`, recorded from main's unmigrated product code before any refactor: the protocol-gate hook, the retrying capability probe, the pairing candidate race, credential rotation, direct-to-relay upgrade, startup pairing recovery and first pairing. The relay modules build their `defaultDependencies` at module scope, so merely referencing `Platform.OS` or a storage-backed loader threw before an adapter could override it. `native-mounting-substitutes.ts` separates reference from use: react, zod and @noble/hashes are the real libraries, expo-crypto routes through the Web Crypto the scheduler already pins, and the two secret stores throw when called. `baseline` repins to |
||
|
|
44268d9616 |
refactor(mobile): send the github.* PR surface and the diff-review loaders through typed RpcOperations (#20668)
* test(mobile): record main's github.* PR and diff-review loaders before migrating them
Scenarios and goldens for the step-4 `src/session/` first half, recorded
against main's unmigrated product code so the migration that follows has a
frozen parity oracle instead of an assertion.
- 20 scenarios over seven new families: the seven `github.*` PR reads, the
twelve PR mutations split by their three reply contracts (`{ok}` envelope,
bare boolean, slug-addressed comment edit), the triage createTerminal+send
launch, the PR branch-context chain and the review screen's three loaders.
- Two new sender-style mount adapters. Both mount exported async functions
taking a client, so no React host is needed and the recorded state is each
wrapper's own outcome.
- 50 new goldens: 20 pilot, 30 reply-matrix sites. `recorderSha256` moved on
all 153 existing goldens because the adapters are in the whole-recorder
digest; no other line in any of them changed.
Text diffs are deliberately unscripted: highlighting one reaches `lowlight`,
which the module loader refuses as an unspecified native dependency.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): send the github.* PR surface and the review loaders through RpcOperation
The step-4 first half for `src/session/`: eight files, 38 references to the raw
request port, all replaced with declared operations. No behaviour change — the
50 goldens recorded in the previous commit do not move, which is the claim.
- 21 operations over 21 methods. The seven PR reads keep their defensive
parsers as readers; the ten status-envelope mutations share one reader
because the `{ok, error}` convention is one host convention, not ten; the two
bare-boolean mutations read the payload unchecked because `=== true` is the
caller's confirmation rule.
- Four second readers, each justified in place: git.status and git.branchCompare
for the PR branch context (a refusal costs a fallback, not the screen),
git.branchCompare and git.branchDiff for review (the projection is not a
superset of the verbatim payload), and worktree.show for the review notes the
summary reader drops.
- Every failure text is preserved, including the two main kept apart: a refusal
with no message falls back to the screen's copy, a transport drop with no
message surfaces its empty message verbatim. `sendRaw`'s callers replaced
theirs a second time, so those fall back on both paths.
- No retry, and no operation reads a dropped reply as a failed mutation: the
rejection reaches each wrapper's catch as the original object.
- `github-pr-mutations.ts` split along the action/comment seam it already had
in its consumers, so no file needs a max-lines bump.
Inventory: src/session/ 47 files / 114 references -> 39 / 76.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): record the review snapshot answering its notes leg first
The barrier mutation census found one survivor: moving
`reviewWorktreeMetadataRead.interpret` inside the `Promise.all` in
`loadMobileDiffReviewSnapshot` changed nothing any golden observed. The base
scenario answers the branch-base legs before the notes leg, so by the time the
notes reply lands the compare leg has already sent `git.branchCompare` and the
two orders record the same sender list.
This scenario answers the notes leg first, while the compare leg is still
resolving its base ref, and checkpoints before the rest. At that checkpoint the
barrier is the whole difference: the correct order has nothing settled, the
early interpretation has already rejected the action. The mutation now fails it.
Recorded from a detached checkout of the previous commit, which carries main's
unmigrated product code with this branch's recorder over it, so the parity claim
stays non-circular. One new golden; no existing golden moved, because the family
base is unchanged and `scenarioSha256` is per golden.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): bind one git.status projection reader, not a copy per domain
The branch-context read declared its own `statusProjectionReader` with the same
parser, the same 'normalized-status' variant and the same empty salvage as
source-control's `gitStatusProjectionReader`, while its doc block claimed "one
reader serves both". Export the source-control reader and bind it here so the
claim is true; the doc now names the reader and keeps the part that is actually
different, which is what a refusal means on each policy.
No wire change and no golden moves: the reader is the same function value the
copy computed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): undo the github-pr-mutations split, which max-lines no longer forces
The split was made when the migrated file measured 319 lines. It does not any
more: `sendRaw`, `sendGithubPrMutation` and `extractMutationError` moved to
github-pr-mutation-outcome.ts and the prRepo/headSha allow-lists to
github-pr-repo-slug.ts, so the merged file is 293 lines against the 300 limit
and oxlint is clean.
Nothing imported github-pr-comment-mutations directly — every consumer went
through the re-export hub in github-pr-mutations — so the seam bought a reader
one extra file to open and nothing else. Merge it back and drop the hub.
Product-only: same wrappers, same params, same settle shapes, no golden moves.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): one settleable-operation type for the PR reads and mutations
`GitHubPrMutationOperation` and the private `GitHubPrReadOperation` declared the
same two members for the same reason: a settle shape needs a bound operation's
method and its interpret, nothing else. Keep one, `GitHubPrSettleableOperation`,
and import it into the read settle. `extractMutationError` goes back to private,
as it was on main; it never had an importer outside its own file.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): drop the key-order claim from the PR param builder
The oracle does not observe param key order: `captureValue` in recording-values.ts
sorts keys, and no golden carries a raw frame string, so "the sender recordings
pin the bytes" was not a fact the evidence supports. The assertion stays for the
reason already in the doc — the builder is method-generic and returns a record.
`GitHubPrParamOptions` goes back to private; nothing outside the module names it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read the bare-boolean mutations with the shared unchecked reader
`mutationConfirmationReader` spelled out what `rpcUncheckedPayloadReader` already
returns, under the same 'pr-mutation-confirmation' variant that eleven other
operations in this tree get from the helper. Same function value, same variant,
so no golden moves. The comment explaining why the payload is left unread stays.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): one RpcOperationSender for both domains, not one alias each
`MobileSessionRpcSender` and `MobileSourceControlRpcSender` were the same type
with the same doc, each derived from whichever operation its domain happened to
own. Replace both with `RpcOperationSender` in transport, derived from
`settingsRead` there, and name it for what it is: what a bound operation needs
to send with.
Still derived rather than restated, so no module names the raw request port to
accept a client; the port inventory and its ratchet are untouched.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): point the moved PR and diff-review adapters at the seam and register them
The merge commit carried the two adapter files into adapters/ with their old
specifiers and left the register untouched, so this completes the move: the
relative imports climb one more level, and both modules are registered in
adapters/mounted-operation-modules.ts as identifiers imported from their own
source, which is what adapter-seam.test.ts checks.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the session goldens against #20662's adapter seam
The merge brought #20568's per-golden scenario digest and #20662's per-golden
adapter digest, so the 51 goldens this PR owns move on four header fields and
nothing else: baseline, goldenFormatVersion, recorderSha256, and the newly
added adapterSha256. No recorded byte outside those headers changed.
baseline stays at main's own pin
|
||
|
|
f742ab88d2 | Update README downloads badge | ||
|
|
9ab0a18e82 |
refactor(agent-status): isolate legacy status ingress behind one admission point (#20716)
* refactor(agent-status): isolate legacy status ingress * fix(agent-hooks): move advertised-capability source onto the ingest envelope ingestRemote() gained a third positional argument in this PR (advertisedAgentStatusCapabilities) to satisfy a new ratchet requiring every legacy-ingress call site to name its capability source. Both production callers pass the same constant every time, so the argument carries zero runtime information — but Vitest's toHaveBeenCalledWith matches argument count exactly, so the pre-existing SSH relay integration test (which asserts a 2-argument call) started failing even though nothing about the actual admission decision changed. Capabilities are a property of the producing peer/connection, not an orthogonal call parameter, so move the field onto the envelope object instead of adding a third positional argument: ingestRemote reads envelope.advertisedAgentStatusCapabilities (defaulting to the unadvertised-legacy-peer set), and both call sites stamp the constant onto their envelope literal. Call arity stays at two arguments, so the pre-existing evidence test needs no change. The envelope never crosses the wire in either caller: SSH rebuilds it field-by-field from the RPC params, and the WSL path copies (never mutates) the wire-deserialized notification before stamping the field on, so this is purely an internal main-process shape change. Also strengthens the ingress ratchet test that required this: it previously only checked that the capability constant's name appeared somewhere in each caller's source, which a stray unused import could satisfy. It now asserts the actual `advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES` key:value binding is present. |
||
|
|
36ef93a64f |
refactor(mobile): migrate the small domains onto RpcOperation (step 4) (#20705)
* refactor(mobile): pin each RPC golden to its own mount adapter, not every domain's `recorderSha256` covered the whole recorder directory, mount adapters included, so a domain PR that adds its adapter module moved the header of all 153 goldens. #20568 did exactly that and its merge with main conflicted on that one line in 153 files; every future domain PR would collide with every other in flight the same way. Split the directory at a real seam instead of a filename convention: `adapters/` holds one module per domain, registered in `adapters/mounted-operation-modules.ts`, and `recorderSha256` now covers the engine only. A new `adapterSha256` covers the source of the module that mounts each operation a golden's scenarios drive, read off the same `mounts` calls that build the table the recording runs against, so the pin cannot name a file the runner did not use. Adding a domain's module now re-digests nothing already recorded; editing one fails exactly the goldens mounted through it. `adapter-seam.test.ts` keeps the split from drifting: an engine file inside `adapters/`, an adapter defined in an engine file, a register entry naming the wrong file, and an adapter importing a sibling each fail. The five adapters that were inline in `pilot-mount-adapters.ts` move into their own modules, which leaves that file as the registry and nothing else. `GOLDEN_FORMAT_VERSION` goes to 5 for the new header field; the goldens re-record in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the split recorder/adapter digest Header-only. Every changed line is `recorderSha256` (the engine digest no longer covers `adapters/`), the new `adapterSha256`, or `goldenFormatVersion` 4 -> 5; `baseline` is unchanged and recording ran against the same pinned product tree. git diff -U0 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256|goldenFormatVersion)":' | wc -l 0 The seven `adapterSha256` values partition the 153 goldens by the module each was recorded through: 58 settings, 37 hosted review, 21 source control, 11 new-tab agents, 9 file inventory, 9 tasks, 8 workspace settings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): stop pinning goldens to recorder inputs no recording can read The adapter split left three per-domain edits still moving all 153 headers: the mutant table, the per-family mutant registry beside it, and the probe-hole witness. None can change a recording -- the loader consults a mutant only when a mutant test asks for one, and no suite but the two recording drivers writes a golden -- so pinning them claimed a provenance the goldens do not have and charged every domain a full re-record for it. `mutants/` now holds the table, the registry, the reference states, the mutant suites and the probe-hole witness, and `recorderSha256` skips it. What makes that sound is that no recording can reach it: `operationModuleLoader` takes a resolved mutation spec instead of importing a table by name, so nothing on the recording path names `mutants/` at all. `mutants/mutant-seam.test.ts` checks exactly that, and fails if an engine file names the directory or anything outside imports from it. `recorderSha256` also pins only the suites in `recording-drivers.ts`, which `scripts/rpc-recording.mts` records from, so the two cannot drift. A suite that reads goldens, or writes one to a scratch directory, is no longer provenance for a recorded file. `OPERATION_EXPOSURES` went the other way, because it does change what a recording loads: withhold the resume-metadata exposure and exactly four goldens fail. Each domain module now declares its own exposures and gets its own loader, so `adapterSha256` pins the ones that reached each golden. Two assertions in the digest boundary test were vacuous: `join(root, '.')` normalises back to `root` and hit `recorderSha256`'s per-root cache, so the prose-is-ignored claim never recomputed anything. Each call now spells the root differently. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the mutant and driver exclusions Header-only, and no format bump: the header shape is unchanged. `recorderSha256` moves on all 153 because the engine set shrank, and `adapterSha256` moves on the 58 settings goldens because that module now carries its own exposure declaration. git diff -U0 HEAD~1 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256)":' | wc -l 0 Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): restore the preferences actions the merge resolution dropped #20568 added `resume` and `trust` actions to the `settings.task-preferences` adapter while it still lived in `pilot-mount-adapters.ts`. This branch had already moved that adapter into `adapters/task-mount-adapters.ts`, so resolving the `pilot-mount-adapters.ts` conflict in favour of the registry merge silently discarded them and `tw-task-preferences-resume-write` failed to record at all ("Missing or completed request: ui.set#1"). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens at main's tip after the merge All 208 goldens, header-only. `baseline` moves from |
||
|
|
caa465d1da |
fix(automations): stop tick latency counting against the missed-run grace (#20819)
* fix(automations): stop tick latency counting against the missed-run grace The scheduler compared wall-clock lateness straight against the grace budget, but evaluation runs on a fixed 60s interval that is never aligned to an occurrence. With grace 0, any tick arriving after the scheduled instant -- in practice every tick -- recorded skipped_missed and told the user "Orca was unavailable during the missed-run grace window" while Orca had been up the whole time. A zero-grace automation effectively never ran. Grace is a downtime catch-up budget. An occurrence that came due while the scheduler was running was never missed; it is waiting for the next tick. The service now tracks continuous availability and only charges lateness to grace for occurrences that came due while it was stopped. Downtime behaviour is unchanged, and the new test asserts that half too. The missed-run branch moved to dispatch-refusal.ts, which already owns non-dispatch outcomes, keeping service.ts under max-lines without a disable. Fixes #11299 * fix(automations): use a tick-latency tolerance instead of process liveness Review caught two real defects in the first cut: - availableSince is process liveness, not continuous execution. A suspended process (system sleep) keeps its start time, so an occurrence that came due during a multi-hour sleep skipped the grace check entirely and replayed on wake -- exactly the downtime case grace exists for. - The restart edge: an occurrence due after the last tick but before stop() was reclassified as downtime and skipped with zero grace. Elapsed lateness cannot be faked by suspension and needs no restart bookkeeping, so the budget is now grace + two tick intervals. Both edges disappear rather than being special-cased. Also fixes a hollow test: workspaceId 'wt1' has no worktree separator, so the target refused and the run recorded skipped_unavailable -- a 'not skipped_missed' assertion passed without ever dispatching. Tests now use a valid id and assert 'dispatching' directly, and cover the sleep, tolerance boundary and restart cases. * fix(automations): scope to the verified tolerance and document the stall gap Review found three defects, all real: - The 'as never' cast failed the changed-code casting gate. AutomationRendererChannel is a Pick<> precisely so a test can pass the real shape; cast removed. - The restart test never restarted: evaluateAt advanced 60s internally, so the first pass already dispatched and the second was a no-op. It now evaluates exactly once and asserts no run exists before the second pass. - tickMs * 2 does not bound a pass that holds the re-entrancy guard across a slow serve-mode dispatch. I tried a busy-window fix for the third and could not test it honestly -- the case needs a genuinely slow in-pass dispatch, and both attempts passed with the fix disabled. Rather than ship logic I cannot prove, the tolerance stays at the verified shape and the gap is documented where the next reader will find it, with the reason 'time since last pass' is the wrong bound (a suspended process runs no passes either). Not a regression: on main that automation never ran at all. * fix(automations): name the check for what it does and correct its message Two review points, both fair: - missedDuringDowntime consulted nothing about availability once the liveness flag was removed; it is elapsed lateness against grace plus tolerance. Renamed missedBeyondGrace so callers read the real contract. - The run error still claimed 'Orca was unavailable' -- the same false statement #11299 was filed about, now reachable for a genuinely late run rather than a merely tick-delayed one. It states what was actually observed instead. Also documented the deliberate trade CodeRabbit raised: elapsed lateness cannot tell a short outage from a late tick, so a zero-grace run due during an outage shorter than the tolerance dispatches instead of skipping. The alternative got the far worse case wrong -- a multi-hour sleep replayed on wake. |
||
|
|
231e805b1e |
fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.
What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.
"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.
Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").
Fix pattern
-----------
Rename for the domain role, not the structure:
-type FieldShape = 'list' | 'map' | 'whole'
-const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
+type FieldEncoding = 'list' | 'map' | 'whole'
+const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>
-function assertGitPushTargetShape(target: unknown): void
+function assertValidGitPushTarget(target: unknown): void
-function describeReadDirPathShape(p: string): ReadDirPathKind
+function classifyReadDirPath(p: string): ReadDirPathKind
Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).
No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.
Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.
* src/renderer/src/components/browser-pane/annotate/**:
in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
rect, ellipse, highlight. That is a genuine domain noun, and it pervades
every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
lucide exports the icon component as `Shapes`. The name is theirs, and the
matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
`shapedSidebar` is a persisted onboarding-checklist field and a telemetry
enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
property is what selects the ZodObject branch of the conditional type.
No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.
Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.
Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.
Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
|
||
|
|
bfdec26352 |
fix(lint): enable anti-slop/no-object-parameters (#20781)
The rule rejects the broad `object` type on any function input (declarations, expressions, arrows, methods, call/construct signatures, function types), plus local aliases and unions that resolve to `object`. `object` accepts every non-primitive while exposing no properties, so it documents nothing and pushes callers into assertions at the boundary. Fixes all 185 violations across src, config, tests and mobile, and flips the rule from "off" to "error" in config/oxlint-anti-slop.json. Approach: replace each `object` input with the type its owner already has. Most sites took an existing domain type or a type-only import (36 added); 40 new aliases name shapes that had none. Where a value is genuinely only compared by reference, it gets a named identity token instead of a shape -- `Record<string, never>`, the built-in `WeakKey`, or a `unique symbol` brand, matching the branding already used in src/shared. Same treatment for WeakMap and Map key parameters. Two `as unknown as` casts became unnecessary once the parameter carried a real type and were removed; no new casts were added. Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no max-lines disable or per-file bump. Three files sat exactly at their max-lines cap, so the added type imports were made line-neutral rather than suppressed: - src/main/ipc/browser.ts exports the existing guest-registration args type (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line. - pane-scroll.ts takes TerminalScrollIntentTarget through the existing pane-manager-types import via a type-only re-export. - direct-rpc-client.ts drops the identity parameter entirely: the session check moved into the sendProbe callback that owns the token. Verified: anti-slop config reports zero violations over src config tests mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no runnable test/typecheck target in this worktree (expo is not installed), so its 6 files were typechecked against a standalone config and diffed against the base branch -- error sets are byte-identical, including test files. |