mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
0cd05bc3d983ef2ff8dd1954161e1e7bb485226b
11077
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0cd05bc3d9 |
docs(contributing): state what a PR description must cover (#21080)
AGENTS.md said nothing about writing PRs, and the template's section comments could be satisfied without ever telling a reviewer what changed for the user or which mechanism moved. Name the same four requirements in both places: no jargon, user-facing before/after, the mechanism, and why over the alternatives. |
||
|
|
12d744f253 |
fix(skills): keep computer-use off filesystem and shell tasks (#21069)
* fix(skills): keep computer-use off filesystem and shell tasks STA-7615: "On my desktop create a folder" was matching computer-use because discovery copy said OS/window-level and neighboring skills advertised desktop UI. Scope the trigger to visible GUI with no CLI path, and exclude files/folders/git/shell. * fix(skills): prefer programmatic paths over computer-use State the last-resort rule in discovery copy instead of enumerating files/folders/git/shell. computer-use prefers shell, filesystem, git, HTTP, CLIs, and Playwright/CDP; neighboring skills route to Computer Use only when a visible window needs GUI control those cannot do. * fix(skills): stop advertising computer-use from orchestration Orchestration coordinates workers; it does not drive a GUI. Drop Computer Use and Playwright/embedded-browser routing from its discovery description so those tools are not pulled in from a coordination skill. * fix(skills): drop Playwright from orca-cli discovery orca-cli should not prescribe Playwright or CDP. Those tools may not be installed, and page automation is not this skill's job. * fix(skills): drop the page-only ban from computer-use discovery Page automation is a preference, not a prohibition. If Playwright or CDP is not available, a visible browser window is valid Computer Use. Keep the hard split for Orca's embedded browser (`orca-cli`) only. |
||
|
|
71e308e574 |
feat(relay): count failed cell-inventory lock acquisitions (#21067)
* feat(relay): count failed cell-inventory lock acquisitions The cell inventory lock is taken NOWAIT, so contention errors with 55P03 and retries instead of waiting. CellInventoryHoldSamples.record only runs after a successful acquisition, so the hold metrics were structurally blind to the dominant failure mode: production showed ~65 failed fleet-wide acquisitions per minute while cellInventoryHoldMsMax read a benign 53ms mean. Count failures next to the holds and publish them as cellInventoryLockUnavailable in orca_relay_runtime_metrics. Drained on both the commit and the rollback path, since a 55P03 rolls its transaction back. * fix(relay): separate request-path lock timeouts from sweep deferrals Review caught that the first counter only incremented under failIfUnavailable, which is the sweep mode. Background sweeps take the inventory NOWAIT and re-derive a skipped candidate next tick, so those deferrals are by design and already reported as orca_relay_sweep_cell_inventory_busy. The request path uses a bounded lock_timeout instead, whose expiry raises the same 55P03 without NOWAIT and was not counted at all -- so the metric measured only the benign population and missed the user-visible one. Split them: cellInventoryLockUnavailable for NOWAIT deferrals, cellInventoryLockTimeouts for expired bounded waits. Production over 30 minutes shows why the distinction matters -- roughly 1,200 fleet-wide sweep deferrals against roughly 10/min request-path timeouts. Adds transaction-path coverage for both drains, which were previously unpinned. Timeouts count per attempt, not per request, since 55P03 is retryable. * fix(relay): publish the cell-inventory lock metrics to Cloud Monitoring google_logging_metric.relay_snapshot only creates metrics for fields listed in relay_runtime_metrics, and the cellInventoryHold* fields were never added when the hold telemetry landed. They have been log-only since, so nothing could alert on the lock and the contention stayed invisible in exactly the way the telemetry was meant to prevent. Maps the three hold fields and both new failure counters. Also corrects the field comment: the split is by wait policy, not by caller. assignOnce takes the inventory fail-fast on its first placement attempt, so request-reachable sites land in cellInventoryLockUnavailable too; that lane reads as contention pressure, and the expired bounded wait is the stall lane. |
||
|
|
f02d09c1ba |
fix(native-chat): deliver queued messages while the chat pane is hidden (#20659)
* fix(native-chat): deliver queued messages while the chat pane is hidden With two or more messages queued, everything behind the head waited on the user's attention. The drain only inspected the head and returned unless it was `queued`, and a `pending` send deliberately leaves the head `dispatching`. An entry only leaves that state through the journal subscription, which is torn down when the pane goes hidden -- and a worktree switch hides it. Two changes, both needed: - One shared admission rule now says what the queue does next, and the drain takes its `dispatch`: the first `queued` entry, skipping entries the host has already acknowledged. It still stops at an `unconfirmed` entry or a refusal the user must act on. Order is not the outbox's to keep -- the host appends the submission inside the per-session serialize chain before dispatching, so journal order is arrival order. Holding the tail bought no ordering guarantee and cost delivery. Single-flight still keeps sends strictly sequential, and a launch prompt's in-flight send, which runs outside it, still stops the queue. - The journal subscription now stays open while a session has undelivered outbox entries, published from the `writeOutbox` choke point. The subscription's retaining hold is what also keeps the host from evicting the session 15s after the last turn, which would otherwise turn the stall into a blocked head refusing `agent_session_ownership_unknown`. An acknowledged entry stays in the outbox rather than retiring on `pending`: the text is safe either way, since the journal upserts a render item from the submission's own body, but a `pending` can still settle `rejected` or `unknown` and only the entry carries the retry state that answer needs. Follow-on corrections the head-only assumption had hidden: - Single-flight is released where the disposition is applied, not in a later `.finally`. That state write is what re-runs the drain, so the release has to land first or the queue has no trigger left. - One ref now holds the in-flight entry's id instead of a bare boolean, and the reconcile effect keys its release on that, not on the head, so a journal update about the head can no longer discard a still-unsettled send of the tail. - A refusal blocks the entry it refused, read back by index so a rotated id is preserved. - The automatic unknown probe and the Retry affordance both read the blocker at whatever index it sits, the Retry through the same shared rule as the drain. `raises no delivery notice for a stuck message behind a healthy head` asserted that a message behind an admitted head raises nothing, because a Retry could not act on it. It now can, so that guard is rewritten to assert the notice names that entry and its Retry sends that entry. * fix(native-chat): resume outbox after journal admission and scope subscriptions * test: name outbox send request by domain role |
||
|
|
36cdb34097 |
test(agent-status): pin each legacy-bypass detector to its own case (#21004)
The ratchet's planted-fixture test collapsed every detection into a deduplicated kind set, so `passed-map` — which has two independent producing sites — stayed green when either one broke on its own. Give each planted form its own case with an exact expected detection. |
||
|
|
383c543e0f |
test(mobile): repin the recording baseline to main after #20950 (#21065)
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
a28085adbf |
refactor(mobile): checked reply readers for the source-control domain (step 7 pilot) (#20950)
* test(mobile): ratchet the 201 unchecked RPC reply readers Step 4 moved every call-site cast into an RpcOperation's `read`, but 201 of those readers still answer `compatible: true` for any payload: `rpcUncheckedPayloadReader` (163), `rpcReadUnchecked` (26 outside its own module) and `rpcUncheckedMemberReader` (12), across 42 files. The cast moved; it did not become true. Held as data with an AST boundary test, shaped on the raw-request-port ratchet: a file that is not listed fails, a listed file that no longer has one fails, and a count that rises fails. Only a call counts, so an import is not a reader and prose never is. No behaviour change: this commit adds a list and a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): validate the source-control domain's RPC replies at arrival Replaces all 17 unchecked readers in mobile/src/source-control/ with `rpcResultVariant(variant, schema)`, so a malformed reply is an `RpcIncompatibleReplyError` naming the operation instead of a TypeError three frames downstream. The inventory drops 201 -> 184 and the five source-control operations files leave it entirely. This is a behaviour change, scoped to malformed replies. Six reply-matrix goldens move; every named-scenario golden and every `normal` partition is byte-identical, which is the parity claim. Schemas live one module per reply domain, beside the operations that read them: git-status, git-compare, git-history, hosted-review and worktree-metadata. A member is required only where a consumer reads it unguarded, and each schema records the consumer line that justifies it. Nothing is `.strict()`; every reply a consumer publishes verbatim keeps `z.looseObject` so an undeclared host member still passes through. Six replies have no reader anywhere in mobile and get `z.unknown()`, which is the honest schema for them, not a holdout. Three readers stay total by construction, because their contract is that an unreadable reply is a value rather than an error: the `git.status` projection (a null status three screens route on), the `session.tabs.list` reveal (a null list means poll again) and the generated commit message (a screen's copy, never a decode error in a text field). They gain the salvage report, not a verdict. Consumers take the schema's output type, so `MobileGitStatusResult` and the branch-compare aliases now name what mobile reads rather than the desktop aggregate, and seven call-site casts are gone. Three requirements came from the goldens, not from the host types: `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the six source-control reply-matrix goldens step 7 moves Six goldens, all on malformed partitions. Every named-scenario golden and every `normal` partition is unchanged, which is the parity claim for this step. git.history-read / git.history#1 result-absent, result-null, inner-ok-missing, inner-false-string-error, inner-false-object-error: the load rejected with a TypeError reading 'items' or 'map' off undefined/null; it now rejects with `incompatible_reply: git.history-page (git.history)`. hostedReview.eligibility + create-intent / hostedReview.getCreationEligibility result-absent, result-null, inner-ok-*: the fetch fulfilled with the error envelope itself, re-typed as an eligibility and published into the compose prefill; it now rejects, and both callers already route that to the same "eligibility unavailable" state a null answer produced. hostedReview.create-chain + create-intent / hostedReview.create result-absent, result-null, inner-ok-missing, inner-false-object-error: the create form showed the raw TypeError text "Cannot read properties of undefined (reading 'ok')"; it now shows the incompatible-reply message. Every header digest is unchanged -- baseline, recorder, adapter, scenario and lockfile all match -- so the diff is the behaviour and nothing else. Recorded from this branch into a scratch directory and copied in, because there is no scoped honest alternative: scripts/rpc-recording.mts refuses to run unless the product tree equals the pinned baseline, and the README's remedy for an intended behaviour change is to repin, which rewrites the `baseline` header of all 667 goldens. So these six now carry a pin whose tree no longer produces them. That is a real gap in the oracle's design for behaviour changes, not a detail of this step, and it needs a decision before this lands. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the four reply-schema properties the goldens found Each of these cost a reply-matrix golden while writing the source-control schemas, and none of them follows from reading the consumers or the host types: a newer host's undeclared members must still decode, `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. The `.strict()` case is the one worth stating twice: at the top level it rejects the reply, and on the entry it drops the row, which shows a dirty worktree an empty Changes list. The fifth test pins the salvage report that makes such a drop visible instead of silent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give an unreadable reply a message a user can read `RpcIncompatibleReplyError` put `incompatible_reply: <op> (<method>)` in `message`, and `message` is what the screens hand to a toast. Step 7 is the first change that can reach this error at all, so the token would have shipped to users as its own error copy. Fixed at the boundary rather than per site: `message` is now plain copy, and the machine token moved to `code` (`incompatible_reply`) and `name` (`RpcIncompatibleReplyError`), both readable by callers. The cross-bundle fallback in `isRpcIncompatibleReplyError` matched on the old message prefix, so it now matches on `name`, which a foreign copy of the module still carries. No existing test pinned the old text. Two new ones pin the copy, the token and the foreign-copy match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to this branch and re-record Commit |
||
|
|
740887fbbb |
feat(settings): connected computers rows for session history indexing (#20887)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * feat(session-search): search every computer from the history panel The panel's "All computers" scope produced no request: the hook parsed the scope into a single host id and stopped when that was null, so the panel answered "Choose one computer to search its sessions." The desktop already merges every enumerated host behind `aiVault:searchSessions`, so pass the scope straight through and stamp each hit with the host it came back on. Hosts the merge could not search are named under the results header with a short reason, since a silent partial answer reads as "no such session". (cherry picked from commit |
||
|
|
aee98ccaa0 |
fix(browser): make the browser identity one process-wide choice (#13822) (#20767)
* feat(browser): process-wide browser identity, chosen before ready
Electron resolves worker identity from a single process-global default, so two
coherent identities cannot coexist in one process. This makes clean/native one
app-wide decision read before `ready`, instead of a per-profile one that leaves
documents on one identity and every worker request on the other.
Both identities are load-bearing, measured across four origins at five reps:
the cleaned identity clears an embedded Turnstile widget and WhatsApp's browser
check where native is refused; native clears a full-page Cloudflare interstitial
that the cleaned identity never clears.
Base commit only: removing the per-profile field, its settings surface, and the
migration notice follow.
* test(browser): cover cross-context UA wire identity
* refactor(browser): make user agent identity app-wide
* test(browser): repair process identity wire fixture
* Fix browser identity startup migration failures
* WIP: rescue in-flight reduced-design work from a dead worker
Worker ctx_cb5b1262d7fe stopped ~2h ago mid-implementation (last heartbeat
2026-09-14T22:48:06Z) leaving this uncommitted. Committed unverified to make it
recoverable; not reviewed, not necessarily green.
* fix(browser): repair the rescued identity work so it typechecks
Finishes the interrupted edits in
|
||
|
|
bdb18003e0 |
test: add accumulated-workspace terminal typing reproduction (#20934)
* test: reproduce accumulated-workspace typing latency through real PTYs * test: make the bench harness self-checks falsifiable Review found four assertions that could not fail and one fixture gap: - `missingPtyArrivalCount`/`missingEchoCount` were hardcoded `0` and `validateExpectedSeqs` throws before them, so every assertion on them was vacuous and every report read `0`. The throw is the real guard and is already covered; drop the vestigial fields. - An absent status controller returned an all-zero result, which satisfied its own accepted-equals-generated equality. Assert presence first. - The byte-pacing control had only an upper bound, so a generator emitting no stream bytes passed. Add the lower bound. - `lineageEvery: 1` built zero lineage: no ordinal satisfies `% 1 === 1`. Offset the interval and cover the densest setting. - The documented control command never set ORCA_TYPING_BENCH, so it skipped instead of running. |
||
|
|
7c4325457c |
fix: name the notification project by the worktree's own host (#20958)
* fix: name the notification project by the worktree's own host STA-4343: a worktree id is `repoId::path` with no host component, so the local host and an SSH host publish one id for two different workspaces. The id-keyed worktree map is first-wins and the repo map is last-wins, so a colliding workspace could be labelled with the other host's project and branch. Resolve the owning host first and name nothing when hydrated ownership cannot prove one; an omitted label beats a wrong one. Only the collision branch changes: a single-row id still takes the same map lookups it did before. * fix: name the project by its host when a repo id spans hosts A repo id is registered per host, so two hosts can hold one id at different paths. Their worktree ids are then unique, so the single-row path skipped host resolution and fell back to the id-keyed repo map, which is last-wins — a local worktree got the ssh project's name. Gate the repo lookup on the id actually being ambiguous: one owner keeps the plain map lookup and its cost, and only a spanning id resolves the owning host, naming nothing when ownership is unprovable. |
||
|
|
a01027697c |
feat(session-search): enable indexing on paired servers from a client (#20886)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * feat(session-search): search every computer from the history panel The panel's "All computers" scope produced no request: the hook parsed the scope into a single host id and stopped when that was null, so the panel answered "Choose one computer to search its sessions." The desktop already merges every enumerated host behind `aiVault:searchSessions`, so pass the scope straight through and stamp each hit with the host it came back on. Hosts the merge could not search are named under the results header with a short reason, since a silent partial answer reads as "no such session". (cherry picked from commit |
||
|
|
8153ec2306 |
feat(session-search): search every computer from the history panel (#20885)
* feat(session-search): add ranked history panel search and consent
* test: wait for initial session indexing before refreshing results
* feat(session-history): add local search settings and index controls
* Use shared local host identifier for session index status
* feat(session-search): merge all-computers search across hosts
The `all` scope on `aiVault:searchSessions` now fans out from the desktop
to every host the session list enumerates and merges the pages into one.
Legs run in parallel: the local index through the search service, SSH and
runtime hosts through the existing remote search client.
Two fixed orders, because relevance scores from independent indexes are
not comparable. `newest` asks every leg for recency and k-way merges on
`updatedAt`, nulls last, ties broken on execution host id. `relevance`
rotates hosts in host-id order by their own rank.
The merged cursor is an opaque base64url payload holding each host's
cursor, how many of its current page were already emitted, and the
generation that offset counts into, plus the page size and sort the
cursor belongs to. A host whose index moved is fenced to `stale` and
stops contributing; the rest keep paging. Per-host outcomes ride back on
one new optional `hosts` field on the results response.
`aiVault:searchStatus` with `all` stays refused, and neither the runtime
RPC nor the CLI gains the scope, so a fan-out is never two hops.
* fix(preload): let the search bridge address the all-computers scope
* feat(settings): live index status, enable confirm, advanced delete
* feat(session-search): search every computer from the history panel
The panel's "All computers" scope produced no request: the hook parsed the
scope into a single host id and stopped when that was null, so the panel
answered "Choose one computer to search its sessions." The desktop already
merges every enumerated host behind `aiVault:searchSessions`, so pass the
scope straight through and stamp each hit with the host it came back on.
Hosts the merge could not search are named under the results header with a
short reason, since a silent partial answer reads as "no such session".
(cherry picked from commit
|
||
|
|
3631a1e77f |
feat(cli): show orca search now the settings toggle ships (#20677)
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`. * feat(cli): show orca search now the settings toggle ships * docs(skills): teach the orca-cli guide the search command One section: what orca search covers, one host at a time, scope and narrowing flags, index status before searching, and that a human turns search on. * docs(skills): shape the search section like the other command sections |
||
|
|
46ed53b88a |
feat(session-search): merge all-computers search across hosts (#20670)
* feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(session-search): merge all-computers search across hosts The `all` scope on `aiVault:searchSessions` now fans out from the desktop to every host the session list enumerates and merges the pages into one. Legs run in parallel: the local index through the search service, SSH and runtime hosts through the existing remote search client. Two fixed orders, because relevance scores from independent indexes are not comparable. `newest` asks every leg for recency and k-way merges on `updatedAt`, nulls last, ties broken on execution host id. `relevance` rotates hosts in host-id order by their own rank. The merged cursor is an opaque base64url payload holding each host's cursor, how many of its current page were already emitted, and the generation that offset counts into, plus the page size and sort the cursor belongs to. A host whose index moved is fenced to `stale` and stops contributing; the rest keep paging. Per-host outcomes ride back on one new optional `hosts` field on the results response. `aiVault:searchStatus` with `all` stays refused, and neither the runtime RPC nor the CLI gains the scope, so a fan-out is never two hops. * fix(preload): let the search bridge address the all-computers scope * feat(settings): live index status, enable confirm, advanced delete * fix(settings): let Button and Collapsible own their spacing and type |
||
|
|
f6197dbd36 |
fix(ai-vault): index Codex agent replies whose content blocks are typed Text (#20763)
Codex 0.153+ writes paginated rollouts whose completed agent messages carry content blocks typed `Text`. The transcript reader matched block types case-sensitively, so every assistant turn from those sessions was dropped before it reached the search index while user turns and tool output were kept. Match case-insensitively and bump the index schema so existing indexes are rebuilt with the replies present. |
||
|
|
0d2f7bcea3 |
fix(session-search): index OpenCode SQLite sessions (#20870)
* feat(session-search): index OpenCode SQLite sessions OpenCode sessions live in one SQLite database read on a worker thread, and the worker only ever answered with the newest few messages for the panel preview. The parser therefore published nothing over the transcript channel, so the search index wrote a placeholder row for every OpenCode candidate and no OpenCode message was ever searchable. Adds a `capture` request to the worker protocol that returns the session and every text part of every user/assistant turn from one open of the database. The agent parser asks for it whenever a sink is listening, so OpenCode joins the whole-document sources on the same path as Grok, Cursor and Gemini. The placeholder path (`parserPublishesMessages`, `noteUnreachableParser`) is gone; an OpenCode read that fails now fails like any other file. Bumps the index schema so existing indexes drop their placeholder rows, and adds `sessionsByAgent` to the index status, which is the count that made this bug visible. * test(session-search): assert every source speaks, not every agent OpenCode has two storage shapes, so asking only that some OpenCode session published messages was satisfied by the legacy JSON fixture while every SQLite session in the vault stayed silent. Assert per discovered source and keep the agent-coverage check beside it. * feat(session-search): capture OpenCode tool and reasoning parts Text parts alone left OpenCode behind every file-based provider: a command someone ran, what it printed, and the model's reasoning were all unsearchable. Widens the capture query to text, reasoning and tool parts. Reasoning folds into the turn's own words, the way the shared block list already treats a thinking block. Each tool part becomes one `tool` message carrying the call line and what came back, built with the same `toolCallText` every file provider uses; OpenCode's `filePath` is renamed to the `file_path` spelling that list knows, so a call is findable by its file argument. Adds a decoded-size ceiling beside the existing part ceiling. It is the bound a non-streaming source needs and a streaming one does not: a JSONL provider publishes each message as it reads it, while this one holds a whole session before posting it across the worker boundary. Neither ceiling truncates; both fail the read so it is retried and surfaces. * fix(ai-vault): fail an OpenCode capture it cannot read the message parts of `readOpenCodeSessionMessages` returned an empty list when the message-part schema probe failed. The sink-aware reader treats that as a complete read, so the consumer committed nothing and marked the source `current`: the session stayed out of the index with nothing on its row to say why and no retry. The part limit a few lines below already throws for exactly this reason, so the two now agree. The preview path is unchanged and still degrades to no messages, which is what a list read should do. Also throws from the fixture's `appendOpenCodeSqliteTurn` when the session id names no row, instead of falling back to the fixture epoch and appending orphan messages a test would then assert over. |
||
|
|
b997fcc77a |
fix(session-search): try phrase and AND routes for prose queries before OR (#20754)
* fix(session-search): try phrase and AND routes for prose queries before OR An exact sentence pasted out of a transcript was not returned. The route ladder only ran the phrase and AND rungs for a literal-looking query, so prose fell straight to OR, where the sentence's common words filled the candidate limit with recent sessions and the old session holding the sentence never reached ranking. The planner now carries a `phrase` candidate: the query's tokens in order with stop words kept, which is what the sentence is actually indexed as. The ladder runs phrase then AND over those tokens for every query of two or more tokens. A one-token query still takes the rung only when it looked literal. `incomplete` is reported by the rung that answered rather than accumulated across every rung tried. * fix(session-search): mark a snippet with the route that retrieved it A phrase hit was highlighted with the OR expression over the stop-word stripped terms, so an exact sentence rendered as scattered bold words with its stop words plain. The snippet now uses the expression the route matched by: one run for a phrase, every typed word for AND, the terms for OR. * fix(session-search): repair a prose phrase without dropping its stop words Typo repair re-planned the query from `plan.body`, which prose has already had its stop words removed from. `relay is droppng frames` therefore came back as the plan for `relay dropping frames`, and the phrase rung searched for a sentence nobody wrote: the transcript holds `relay is dropping frames`, so the exact match fell through to AND. The repair now maps over `plan.phrase`, the tokens as typed, and re-plans from those. Only terms the body holds are offered to the corrector, so a stop word is still never repaired, and the re-plan recomputes the body from the corrected sentence exactly as before. |
||
|
|
dbd750f64d |
fix(session-search): keep the index status honest while a sweep has a backlog (#20753)
* fix(session-search): keep the index status honest while a sweep has a backlog A pass stops reading at its wall-clock deadline and records nothing about the candidates it never opened, which is correct: being owed a read is a fact about the row, not an entry in a queue. But a candidate the opening sweep never reached has no row at all, so the store's `due` count cannot see it. The sweep still reported `completed`, the indexer stamped `lastSweepCompletedAt`, and `status()` answered `current` with a backlog of thousands: "Up to date - 130 files indexed", then 630, then more. The read loop now counts what it decided was owed and did not read and hands the number back as `left`; the pass propagates it; the indexer holds the last pass's count, adds it to `filesDue`, reports `indexing` while it is non-zero or a sweep is owed, and no longer stamps a sweep the deadline cut short as complete. * fix(session-search): only an unread backlog keeps the phase at indexing An armed cadence sweep on a drained index is not a backlog, so it no longer flashes the pane to indexing with nothing due. * fix(session-search): stop counting a deferred `due` row twice in filesDue `status()` reports `filesDue` as `stateCounts().due + left`. The read loop incremented `left` for every candidate the deadline cut off, including one whose row already said `due` — and that row is what `stateCounts().due` counts. A sweep that ran out of time therefore reported each already-due transcript twice. `left` now skips a deferred candidate whose row is already `due`. A candidate with no row, and a `current` row whose file moved, still count: those are the backlog no query can see, which is why `left` exists. |
||
|
|
a7e34d5695 |
feat(session-search): add panel search and opt-in consent (PR7) (#20580)
* feat(session-search): add ranked history panel search and consent * test: wait for initial session indexing before refreshing results * fix(lint): drop the type import #20898 left behind in the windowing test main's tip fails `typecheck` and `static analysis` on `NativeChatMessageList.windowing.test.tsx`: #20898 moved the growth/append suite into its own file and took the last use of `NativeChatMessage` with it, leaving the import. Every open PR reds both jobs through the merge ref, so this rides the first branch that has to merge main in. |
||
|
|
f85d2bf6ad |
fix(ai-vault): say OpenCode-in-WSL is not searchable from Windows yet (#20971)
The scan issue for an OpenCode database on a \\wsl.localhost share read as an error with an instruction the user cannot follow. It now surfaces on the Agent Session Search settings page under a computer row, where it belongs as a known limitation, so the WSL branch now reads 'OpenCode sessions inside WSL can't be searched from Windows yet.' Copy only: the issue keeps kind 'scope' and its path, every other branch is untouched, and discovery, the WSL gate, and the busy-timeout behavior are unchanged. |
||
|
|
3e5eb0329a |
feat(cli): orca search over the agent session index (#20514)
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`. |
||
|
|
dec0e2cd56 |
feat(session-history): add local search settings and index controls (#20582)
* feat(session-history): add local search settings and index controls * Use shared local host identifier for session index status * feat(settings): live index status, enable confirm, advanced delete * fix(settings): let Button and Collapsible own their spacing and type |
||
|
|
0b28d354fe | Update README downloads badge | ||
|
|
291b4ddd6f |
feat(agent-status): route structured sessions through canonical ownership (#20718)
* feat(agent-status): route structured status through canonical ownership and fence child lifetimes Restacked onto the canonical store and child-work contract. Completing that restack drops the `reopenStructuredParent` mutation flag this change had carried, along with its contract field, its codec branch, and its single call site in structured ingest, which passed a hardcoded `true`. The flag was a narrow escape hatch from the absolute `tombstones.has(...)` rule that governed parent upserts in this branch's original base. The canonical store replaces that rule with a revision envelope, because a bounded store compacts tombstones away and a presence-based guard silently stops fencing once one is evicted. With the envelope deciding the outcome, the escape hatch has nothing left to escape from, so removing it changes no production behaviour. `agent-status-store-reopen.test.ts` is rewritten against the envelope: the reopen case now pins that an unflagged republication succeeds while replay from before the reopen stays fenced even after the parent tombstone is compacted away, and the second case pins where the guard genuinely bites — a republication inside the removing mutation itself, for every subject kind. * fix(agent-status): re-admit unchanged structured owners after teardown * fix(agent-status): clear anti-slop object-param and Reflect.apply findings - agent-status-store-byte-budget.ts: type the byte-budget helper's record parameter as the union of what its call sites actually pass (the snapshot header plus each store entity record) instead of the broad `object`. - server-structured-canonical-status.test.ts: replace `Reflect.apply` with a typed, explicitly-bound call that models a caller at an untyped boundary omitting the trusted owner subject. * docs(agent-status): drop the 2A progress doc from docs/reference docs/reference/ holds implementation detail, not rollout progress. The canonical-boundary notes move to the effort's working directory; the agent-status-store status section keeps the boundary statement and loses the now-dangling link. * fix(agent-status): mint the canonical epoch on first use, not at construction The hook server's canonical store was built in an instance-member initializer, so constructing AgentHookServer — which happens at import time for the module singleton — demanded a live randomUUID. Any importer that stubs node:crypto threw 'Invalid agent status store epoch' before a single test ran. The store is now created on first canonical access and reset by dropping it, so construction owes nothing to a crypto implementation and the epoch still rotates per authority incarnation. * fix(agent-status): drop the orphaned snapshot budget and a duplicated pane guard Two leftovers from the canonical-store routing change: agent-status-store-snapshot-budget.ts lost its only caller when the store state switched to agentStatusStoreFitsByteBudget. Nothing in the repo imports it now, so the module goes with the caller it existed for. The replacement is not a straight copy: it only memoises a record's measured size once the record is frozen, so a still-mutable record can no longer return a stale byte count. persistedStructuredWorkerPaneKeyIsValid repeated its public-pane-key rejection verbatim three lines below the first one. The tests covering that rejection pass on the first occurrence alone, so the second decided nothing and only obscured which predicate was load-bearing. * fix(agent-status): stop a failed structured publish from latching as owned Three defects found reviewing the structured routing path. combinedStatusEntries defaulted a missing listing order to 0, but the counter it compares against starts at 1, so any unordered row sorted above every ordered one. Unknown order now sorts last. The owner map recorded a session as owned before the sink ran. A publish that threw therefore left matchesLocation reporting an owned location for a row that was never written, and the unchanged-projection path — the only thing that would re-offer it — stopped. The address still has to survive a throw so teardown can forget a row that did land, so the two facts are now separate: the address is recorded up front, and only a publish that returned marks the row as landed. The reopen test claimed the revision envelope rather than the tombstone fences a stale replay. It cannot tell: transport consecutiveness, the parent-revision validator and the tombstone guard each refuse that replay alone, and ablating any two leaves the test green. It now asserts the outcome and says so. |
||
|
|
170ebce1f2 |
fix(ci): run static analysis for every tree the repo-wide audits scan (#20918)
A mobile-only diff is desktop-irrelevant, so should_run was false and every PR check skipped -- including the audits that do lint mobile/. The violation then landed on main and failed the same gate on every later PR's merge ref. Derive the trigger from the audit commands' own scan roots so the two cannot drift. |
||
|
|
15cac68802 |
Native chat keeps scrolling to bottom (#20898)
* fix(native-chat): prevent auto-scroll when transcript is hidden Stop following new messages to bottom when the chat view is not visible (e.g., in an inactive tab). Restore scroll position when the transcript becomes visible again. * fix(native-chat): preserve reader scroll offset when transcript is revea When a reader scrolls away from the latest messages and the chat tab becomes hidden, save their scroll position. If messages arrive while the tab is hidden, don't auto-scroll. When the tab is revealed, restore the saved offset instead of jumping to latest, preserving their reading context across hide/reveal cycles. * refactor(native-chat): extract growth-append tests and status component Move transcript growth/append test suite to dedicated growth-windowing.test.tsx file for better organization. Extract status rendering logic (errors, retry, background tasks) from NativeChatStructuredSession into NativeChatStructuredSessionStatus. Fix scroll offset preservation in test harness when transcript visibility toggles. * refactor(native-chat): remove retry UI Remove unused retry functionality for failed message delivery from the native chat status component. The retryableOutboxEntry state is no longer managed, so the retry button and associated handling can be safely removed. |
||
|
|
07b7687a2e |
fix(sidebar): keep the Projects filter when a project is added (#20987)
Adding a project wiped the Projects filter: both reveal paths made the new project visible by clearing filterRepoIds outright, so a user filtered to A and B was dropped back to every project. The filter is an allow-list, so revealing a repo only needs that repo added to it. revealRepoInProjectFilter widens the selection instead, and no-ops while the filter is off, where adding an id would turn "show everything" into "show only this project". STA-7588 Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
d62328aa4d |
fix(codex): remove redundant Windows hook launcher for Unicode profiles (#20952)
* fix(codex): reuse the Windows hook shell for Unicode profile paths * test(codex): register Unicode hook tests in Windows CI * test(codex): pin trust hash replacement during Windows upgrade * test(codex): retry transient Windows teardown locks |
||
|
|
78609330d1 |
Fix browser viewport presets incorrectly scaled by UI zoom (#20962)
* Fix browser viewport presets scaled incorrectly by UI zoom Browser viewport presets must remain in window DIP (native) coordinates but scale in CSS pixels as UI zoom changes. Store preset dimensions as CSS variables in DIP, then divide by the live UI zoom factor in the stylesheet. Also consolidate zoom factor calculations across the app to use a shared `uiZoomFactorFromLevel()` function and add `windowDipToCssPx()` for converting native coordinates to CSS pixels. * Move viewport preset zoom compensation to CSS class Inline width/height styles outrank class rules, preventing the zoom compensation from applying. Using a class rule ensures the viewport scales correctly as the UI zoom factor changes. |
||
|
|
e39b432c40 |
fix(editor): preserve Markdown scroll after image layout (#20956)
* fix(editor): preserve markdown scroll after image layout * test(editor): harden scroll regression cleanup and geometry checks |
||
|
|
16ac9018db |
docs: remove unavailable diff shortcuts (#20974)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
c702e77bc7 |
Stop reading the terminal arguments field on the structured chat route (#20944)
* fix(native-chat): stop reading the terminal arguments field on the structured chat route Setting Claude's Arguments to "--dangerously-skip-permissions --model Opus" made every new Claude tab open in the old terminal-backed chat instead of the new structured one, with nothing on screen to explain why. Removing "--model Opus" fixed it. The cause was a whole-string comparison: the configured arguments were checked against a single blessed value per agent, so any added token at all — including one the agent supports — stopped the string matching and the launch was demoted. Structured chat does not run the interactive CLI. It drives Claude through the Agent SDK and Codex through app-server, and those take narrower option sets that are versioned separately from the CLI's, so one free-text field cannot have a guaranteed meaning for all three. The structured route now reads only what it can actually honour: a replaced launch command, or a launch that names its own working directory. Terminal launches still apply the field exactly as before. Permission posture no longer travels as a raw flag. It is derived from the resolved launch arguments, which is the same fact a terminal launch acts on and which falls back to the default Orca ships when the field was never touched, so bypass stays on by default and Manual is still honoured. Claude gets the SDK's typed permissionMode and allowDangerouslySkipPermissions at query start; Codex gets its bypass flag placed before the app-server subcommand. Both are re-derived per acquisition beside the auth policy and environment overlay rather than stored in the session record, so nothing can disagree with the setting. Codex also loses the --profile, --add-dir and -c passthrough that reached app-server through that field. Only the permission posture comes back. * test(native-chat): pin routing authority on the narrowed feasibility input The routing-authority pin still named the old bundled blocker and built its "customized" fixture out of the arguments field, which is no longer a feasibility input. Both are now the launch command, and arguments and environment are customized on both passes of the loop, so the flag handed to the shared resolver tracks the command alone — a caller that resumed reading either one fails here. No case is dropped and no assertion is relaxed: the blocker list is still exhaustive and every caller must still honour a refusal from the shared resolver. |
||
|
|
f78483ec29 |
refactor(mobile): send the subscription-gated holdouts through typed RpcOperations (step 6, migration 1) (#20954)
* test(mobile): record the three step-6 families at the pin, and record a stream listener that dies Step 6 migrates the requests step 4 left behind because they share an effect with a `client.subscribe`. This records them first, from the pinned baseline, so the refactor that follows has a parity oracle. Three new families, one adapter module each: - `session.native-chat-page` — the older-history page. The read is a callback, but only the mount effect's `nativeChat.subscribe` arms what it pages against, so the frames are the setup: the snapshot's `beforeOffset` decides whether the request carries a cursor or asks for a growing tail. A cutover and a second snapshot pin the reconnect replay merging into paged-in history instead of collapsing the window. - `notifications.desktop-stream` — the desktop notification socket: the subscribe, the catch-up read its `ready` arms, the tray dismissals its events drive, and the server unsubscribe the disposer sends. Split in two so the base scenario's matrix sites all have partition-stable params: a variant that answers the second `ready` differently leaves the unsubscribe carrying the first subscription id, which the base's scripted params could not assert. - `session.terminal-gesture-input` — the debounced gesture flush and the menu's clear-buffer. Neither rides a subscription; a mount holding no terminal ref reaches both. The engine change is what makes the first two recordable at all. `ScriptedRpcTransport.frame` now returns what the product listener threw instead of throwing it on, and the runner records it as a `stream-listener-crash` effect. Only the two `runtime.clientEvents` listeners check that a frame payload is an object before reading its `type`; every other subscribing family took the matrix's `result-absent` and `result-null` partitions as an uncaught TypeError, which failed the suite rather than recording what a malformed frame does to a subscription. That is the same rule the crash boundary already holds for a screen and the unhandled-rejection window for a detached effect. The scenario's own faults stay loud: a missing subscribe payload, a params mismatch and a closed stream are all raised outside the caught region. `recorderSha256` therefore moves, so all 679 pre-existing goldens are re-recorded from the pin with this branch's recorder laid over it. Every one of them moves exactly one line and that line is `recorderSha256`: no `adapterSha256`, no `scenarioSha256` and no observation moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the subscription-gated holdouts through typed RpcOperations (step 6) Five references over four files leave the raw request port. Each was held out of step 4 because a request-only recorder could not mount it; the recordings landed in the previous commit and no golden moves here. - `use-live-worktree-name.ts` — `worktree.show` inside the focus effect that opens `runtime.clientEvents`. It reuses `sessionWorktreeRecordRead`, which is the diff-comment loader's reader renamed: both consumers read the `worktree` member whole and narrow their own field off it, so a second family would have been a second name for the same wire. The resolution still comes off the raw reply, because `selector_not_found` is what proves the worktree is gone and no acceptance policy carries a refusal code; the skip that follows is the same verdict main's `!response.ok` reached, since a refusal is the only reply this policy declines. - `use-mobile-native-chat-session.ts` — `nativeChat.readSession` in the paging callback. The payload stays whole because the reply is a union: an older runtime answers `{ error }` in place of a window, and the caller discriminates before reading a message list. - `mobile-notifications.ts` — `notifications.unsubscribe` in the `ready` branch of the subscription callback, in its own module rather than beside the push-route sends: one is the route this device holds with a gateway, the other the socket the paired connection holds. - `use-mobile-session-terminal-input.ts` — the gesture flush reuses `terminalInputSend`, which already carried the four other terminal-input call sites and the same accepted-verdict, and the menu's clear gets `terminalBufferClear` beside it. The clear is a skip because main never read the envelope: it toasted success on any fulfilled reply, so only a transport rejection reached the failure toast. That is preserved, not repaired. `mobile-session-route-parity.test.ts` refreshes three pins with their reason: the callback bodies and the twelve nested-function bodies moved where those send expressions were rewritten, and the runtime-string count drops by two because `terminal.send` and `terminal.clearBuffer` are now fixed at their operation's definition instead of spelled at the call site. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): hold the subscription coverage as a checked inventory instead of a README paragraph Every product `client.subscribe` is now an entry in `mobile/src/transport/rpc-subscription-inventory.ts`, classified as recorded (naming its family), an unwritten scenario, or walled with the wall named. `rpc-subscription-boundary.test.ts` fails on a new site with no entry, an entry whose file no longer subscribes, an entry naming a method the file does not open, and a `recorded` entry whose family the scenario manifest does not have. Both the unlisted-site and unresolved-family gates were checked by removing an entry and by misspelling a family; each fails on its own assertion. The paragraph this replaces said nine sites when there were ten. It counted over `mobile/src`, and the host screen's `accounts.subscribe` lives under `app/` — so the scan here covers both roots, the way the raw-port ratchet next door does. Ten sites today: four recorded, two unwritten scenarios, four walled (two on the webview ref, one on the multi-host client context, one on two unsubstituted view members). Unlike the raw-port inventory this list does not count down to zero. A typed operation fixes one method, one acceptance and one reader for one reply; a stream has many, and replacing a subscribe is not what this is asking for. The question it holds is the other one — which stream a golden actually has, and for the rest, what exactly stops it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the notification stream close, which writes nothing to the wire Deleting `unsubscribeStream()` from the notification cleanup — the local close, not the `notifications.unsubscribe` RPC beside it — survived all 810 tests. Neither unsubscribe builder in the stream registry knows `notifications.subscribe`, so closing that stream sends no frame; the mutant leaks a live subscription record instead, and the leak only surfaces when the logical client replays it onto the next session. `notifications-desktop-stream-closed` stops the stream and then cuts over, where the leak becomes a second `notifications.subscribe` payload. Recorded at the pin. No existing golden moves: the new scenario is appended, so it is not the family's matrix base, and every notification matrix site already had a fulfilled reply to replay. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): name the accounts screen's real wall, which is ScrollView and Alert The entry blamed `expo-router.useFocusEffect`, which is substituted, and the inventory's own `use-live-worktree-name` is recorded while importing it. Probed by mounting the screen through the trap: the first refusal is `Unsubstituted native member: react-native.ScrollView`, and `Alert` refuses too. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the terminal-send response reader that lost its last caller `isTerminalSendRpcAccepted` read the verdict off a whole envelope, which is what the raw call site did. Both callers now send through an operation and read the admitted payload, so the response form had only its own test left. The three cases move onto `isTerminalSendResultAccepted`, with the refusal envelope's missing result standing in for the failed response. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): attribute a frame crash to the listener that threw, not to the registry The try wrapped `stream.deliver`, so anything the registry raised on its way to the listener was recorded as a `stream-listener-crash` effect and blamed on the product. A reply like `{ok:false}` with no error object throws reaching for `error.message` before any listener runs, and that is a scenario that stopped matching, not an observation. Only the product's own `onData` is wrapped now. The throw is stashed and rethrown unchanged, so the registry still sees it the way a device's message handler does and what it skips after a dead listener stays recorded rather than invented; `frame` reports it only when the error it caught is the one the listener raised. `FrameListenerCrash` is local to the file again. Engine change, so every golden re-records: 694 files, every changed line the `recorderSha256` header, no body movement. Against main the set is 679 modified header-only and the same 15 added. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read the frame listener stash through a method, not a narrowed field `this.listenerCrash = null` before the try narrows the property to `null` for the rest of `frame`, so the catch compared against `never` and mobile's own `tsc --noEmit` failed. A private taker returns the declared type. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): abort a registry throw that stashed nothing, and fold the last native-chat read module in The frame catch compared `crashed?.error !== error`, which is false when nothing was stashed and the registry threw `undefined`, so that abort was swallowed and `frame` reported a clean delivery. It now asks whether a listener crashed at all. Also: `nativeChatSessionPageRead` moves beside the three other `nativeChat.*` reads and its one-export module goes; the session read header names the whole `worktree.show` record rather than review notes; the guarded-listener count is three, not two; the README names the ten subscribing sites blur is unrecorded across; and the gesture flush reads the send verdict as `=== true` like the other four sites. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore(mobile): drop an oxlint disable the rule never needed `no-throw-literal` is not enabled here, so the directive read as unused and failed the changed-code quality gate. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
4948afc2ec |
fix(linear): show 'Cannot verify' when skill scan is inconclusive (#20964)
A scan that encounters an error before discovering skills, or hits an unreadable root, cannot vouch for "not installed". Previously these cases were conflated with proven absence, so the checklist would claim the skill step incomplete even when all three steps had been finished. Now the UI distinguishes between confirmed states and unknown ones, showing "Cannot verify" instead of listing the skill as an unfinished step. |
||
|
|
feb04ec254 |
Virtualize automations run history table for large histories (#20916)
* refactor(automations): virtualize run history table - Add virtual scrolling to AutomationRunHistory for efficient rendering of large run lists - Implement sticky table header that stays visible during scroll - Update keyboard navigation and focus management for virtualized rows - Move AutomationRunsTable header inside scroll container for visual consistency - Add virtualizer-test-stub for testing virtual scroll behavior without DOM measurement - Cache DateTimeFormat to avoid per-cell allocation overhead * test(automations): add coverage for virtualized run table - Tests verify row content renders spend, tokens, and workspace labels correctly - Keyboard navigation guards prevent operations during failed host reads - Load-more pagination triggers at scroll end and respects page boundaries - New fixtures support flexible automation run and usage test scenarios * test(automations): verify scroll-to-focus path in virtualized runs - Implement scrollToIndex in virtualizer stub to move viewport window - Optimize row-size estimation to use predicate instead of labels - Test validates keyboard navigation scrolls rows into view before focus * add more tests |
||
|
|
b0d46e2d3d | fix(settings): preserve multiline proxy bypass rules (#20957) | ||
|
|
bfc297df99 |
fix(settings): keep integration connect dialog drafts on backdrop click (#20932)
* fix(settings): keep integration connect dialog drafts on backdrop click A backdrop click in the Settings → Integrations Jira/Linear/Bitbucket connect dialogs dismissed the Radix modal, and each dialog's reset-on-open then wiped the typed credential. Generalize SshTargetForm's dirty-gated outside dismissal into a shared preventOutsideDismissWhenDirty factory and wire it into the three dialogs (and SshTargetForm), so an accidental backdrop click no longer discards a draft while Escape / Cancel / × remain the explicit discard paths. Bitbucket compares email/baseUrl against a props-seeded baseline and only counts the active auth mode's fields, so a mid-edit status refresh and a mode toggle cannot make the form sticky. STA-7332 * test(e2e): drop ticket id from dismiss spec comment |
||
|
|
9ed561c1d2 |
fix(claude): judge Stop against the turn the journal published (#20921)
* fix(claude): judge Stop against the turn the journal published A Stop could be refused for the turn the user was actually looking at. The client derives the id it sends from the published journal rows, but the host compared it against the adapter's own in-memory turn. The journal sink drains asynchronously, so that in-memory value can already name a turn whose row has not landed — an id no client has been shown, and one the client's Stop can therefore never match. The user pressed Stop and nothing stopped. Fix the guard's source rather than the guard. ownsRequestedTurn stays: it is what stops a delayed request from interrupting a later turn, and without it a stale Stop would reach a session-scoped interrupt that settles every queued send as durably rejected. The host now resolves the live turn from the journal projection and hands it to the adapter, which prefers it and falls back to its in-memory read for direct callers that have no journal. It is passed as a read rather than a value because the guard re-checks after the delivery fence may have waited seconds; a value captured at request time would interrupt whatever turn ran next. Both callers supply it. The handoff's own Stop bypasses performCancel, so its body moves into stopNativeHandoffTurn beside the file's other extracted flows, which is also what lets it be tested on its own. * fix(claude): fall back to the in-memory turn while the journal drains The journal drains through a serialized async queue, so a live turn routinely has no published row yet. Judging a Stop only against the journal refused in that window, which gates a user action on bookkeeping. The journal stays authoritative while it HAS an answer; a null read falls through to the in-memory turn, and the nothing-dispatched clause is unchanged. Both call sites now read the live turn through `journal.activeTurnId()`, which folds reduced items instead of rendering and sorting a whole snapshot. |
||
|
|
47bb473ec6 |
Remove agent map from dashboard popout (#20929)
The agent map view was not functional and its components have been removed entirely. The dashboard popout now only supports the kanban board view, with all map-related code, utilities, types, and translations cleaned up accordingly. |
||
|
|
96d77b37c5 |
perf: always show project names and remove notification scans (#20931)
* perf: avoid repeated agent scans when labeling notifications * perf: always label notifications and remove project counting * fix: qualify the notification project group by its folder's host Folder notifications resolved the folder host-aware, then looked its project group up by bare ID. The owner index fails a bare ID closed when two hosts publish the same group ID, so a remote folder lost the project name the catalog already had. Also drops the identity rescans that recovered display fields: the catalog finders now return the caller's row type, matching findIndexedRepoOwnerForHost. Updates the idle-arbitration expectation that still asserted the removed hasMultipleActiveRepos flag. |
||
|
|
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 |