Repository navigation
refactor(mobile): checked reply readers for the session domain (step 7) - #21089
Conversation
…ed read site Step 7 for the session domain changes how 51 RPC readers read a *malformed* reply. Eleven of the session read sites had no recording family, so main's answer to a malformed reply at those sites was undocumented and the reader change would have had nothing to move. This commit is the before picture, taken from main's own tree with no product edit in it. Ten new families, twelve scenarios, twenty-five goldens: - `session.review-file-diff` / `session.review-branch-diff` — `git.diff` and `git.branchDiff` read through the review projection, which the Changes screen's verbatim readers do not cover. - `session.review-git-mutations` — the single-file `git.stage` / `git.discard` and the bulk stage sweep's second `git.stage`. - `session.review-send-sheet` — `session.tabs.list` read for the agent terminals the send sheet lists, the third reader on that method. Needs an `open-send-sheet` action on the review-action adapter, which re-digests that family's eight goldens on `adapterSha256` and nothing else. - `session.browser-tab-create` — `browser.tabCreate`. - `agentSession.structured-create` — `agentSession.create`, whose family base only ever covered the support probe. - `session.tab-rename` / `session.tab-close-session` — `terminal.rename` and `session.tabs.close`. - `settings.new-tab-local-agents` — `preflight.detectAgents`, the arm the new-tab loader takes for a workspace with no connection. `baseline` is repinned to main's tip because two commits (#20659, #21004) touched a fenced path after the pilot's pin, so `--record` refuses on main's own tree until it moves. The repin is what rewrites `baseline` on all 705 existing goldens; nothing else about them moves. Decoded against origin/main through the value pool: 705 header-only (`baseline` on every one, `adapterSha256` on the eight review-action goldens), 0 body-moved, 25 added, 0 deleted. Not covered, with the reason: the chunked clipboard upload's `appendImageUploadChunk`, `commitImageUpload` and `abortImageUpload` cannot be matrixed, because `replyMatrixSites` takes every completion in the base scenario and the chain's later params carry the `uploadId` the start reply named. Driving `clipboard.startImageUpload#1` therefore makes main send an append whose params no scripted step matches, and the recorder raises `Request params mismatch: clipboard.appendImageUploadChunk#1` instead of recording. The two families were written, probed and removed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Fifty-one unchecked reply readers across nine files become checked zod readers,
so a malformed host reply surfaces as one readable error at the operation
boundary instead of a downstream `TypeError`, a rendered `undefined`, or a
screen left ready over garbage. Deliberately a behaviour change on malformed
replies only.
Eight schema modules, one per reply family, each recording the consumer line
behind every requirement and the host handler that publishes it:
- `clipboard-image-reply-schema.ts` — the upload slot's `uploadId`, the commit
and single-frame path strings, and the two legs whose body nothing reads.
- `github-pr-mutation-reply-schema.ts` — the `{ ok, error }` status envelope as
two variants, and the bare-boolean confirmation.
- `github-pr-entity-reply-schema.ts` / `github-pr-read-reply-schema.ts` — the
seven PR sidebar reads. Every identity requirement the hand parsers had is
kept, so a payload that degraded to null still degrades to null; what changes
is a payload that is not the declared container at all.
- `diff-review-reply-schema.ts` — the normalized branch compare, the review
notes on the worktree record, the three file-diff arms, and the file-level git
mutations.
- `review-terminal-reply-schema.ts`, `session-launch-reply-schema.ts`,
`session-read-reply-schema.ts`, `session-write-reply-schema.ts` — the review
send sheet, the launch paths, the session screen's reads and its writes.
Requirements are exactly the members a consumer reads unguarded, everything else
is a salvaged optional with main's own default applied in the transform, and no
schema is `.strict()`: a member a newer host adds passes through untouched.
Enum arm sets that a reader compares against pass through or degrade to the arm
the reader handles most conservatively; the two closed sets — the committed
change status and the diff kind — are closed because main *dropped* an arm it
did not know rather than passing it through, and degrading them would draw a row
or render a diff main never did. No member is coerced on the way back to the
host.
`github-pr-parsers.ts`, `github-pr-comment-parsers.ts` and
`github-pr-value-readers.ts` are gone; their suite is now the parity record for
the schemas that replaced them, with the four cases that refuse rather than
degrade marked as such. Twelve call-site casts are deleted, and three dead
"response was invalid" branches with them: the reader refuses those replies now,
so the error names its method.
The nine session files come off `unchecked-rpc-reader-inventory.ts` entirely
rather than being lowered. `git show --stat` on this commit touches nothing
under `mobile/rpc-foundation`.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Three kinds of case, one per kind of decision the schemas encode: a member a consumer reads unguarded is required and its absence refuses, an arm set a reader compares against degrades to the arm that reader handles most conservatively, and a reply whose arms need different members is declared as variants and each arm is read. The last suite is the wire-compatibility claim: a member no reader knows passes straight through, on the markdown document, the upload slot and the terminal inventory alike, so a newer host is never refused for a field mobile does not read. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…ders
Repins `baseline` to the last commit touching a fenced path and re-records all
730 goldens, which is the disclosed behaviour change taken as an observation.
Decoded through the value pool against the pre-refactor tree on this branch:
688 header-only with `baseline` the only key that moved, 42 body-moved, 0 added,
0 deleted. The 42 are seven named scenarios and thirty-five matrix goldens, and
every moved checkpoint's own reply is malformed or refused. Three `normal`
partitions appear in the list and none of them reads a well-formed reply
differently: the review file-diff family's base scenario drives three legs and
its third is scripted `{ kind: 'unknown' }`, so that leg's checkpoint moves in
every variant, the varied leg included. The same append-only-history effect puts
`pr-read-upstream-error`'s `no-pr` checkpoint in the list for the malformed PR
recorded before it.
What the corpus now records, in one sentence: a property read on null, a V8
destructuring message shown to the user, and four hand-written "response was
invalid" strings are replaced by one message that names the method, and four
screens that published a malformed payload as ready state now show an error
instead.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…emas `github-pr-read-reply-schema.ts` was 328 code lines against the 300-line cap. The expanded check run and the annotations, jobs and steps listed under it are one reply with no reader in common with the other six, so they move to `github-pr-check-reply-schema.ts` whole. A move, not an edit: no schema changes and no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The schema-module split touched `mobile/src`, so `--record` refuses on the pin the previous refresh left behind. Repins to that commit and re-records. Decoded against the previous corpus: 730 header-only with `baseline` the only key that moved, 0 body-moved, 0 added, 0 deleted — the split is a move, and the corpus says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The live-title read is typed by its schema now, so the cast it annotated is gone and the import it needed with it. oxlint flags the leftover. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The unused-import removal touched a fenced path, so the pin moves with it. Decoded against the previous corpus: 730 header-only on `baseline` alone, 0 body-moved, 0 added, 0 deleted. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
ℹ️ No critical issues — one parity suggestion inline.
Reviewed changes
Step 7 converts the session domain's 51 unchecked reply readers to checked zod schemas. Read the whole non-golden diff (51 files) plus the deleted parser modules; one parity sweep over every new schema vs. the parsers it replaces covered the rest.
- 51 readers become schemas across 9
mobile/src/session/operation files (github PR reads/mutations, diff review, review terminal, clipboard image, session read/write/launch);rpcUncheckedPayloadReader/rpcUncheckedMemberReadergive way torpcResultVariant, and 12 call-site casts plus 3 dead "response was invalid" branches are deleted. - Three hand-written parser modules deleted —
github-pr-parsers.ts,github-pr-comment-parsers.ts,github-pr-value-readers.ts— with their 39 expectations rehomed ingithub-pr-rpc.test.tsas the schemas' parity record. - Malformed replies change behaviour deliberately: one method-named
RpcIncompatibleReplyErrorinstead ofnull/[]/ a coerced default / a V8TypeError; the schema unit suite and the 42 moved or added golden families pin the decisions. - The session block left the unchecked-reader inventory and the corpus was regenerated (33 body-moved, 25 added, 672 header-only from the
baselinerepin).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
The checks read was the one phase-1 dependency that could take the whole PR sidebar down. `loadPrSidebarData` routed `!checksOutcome.ok` through `failureState`, so a host whose `github.prChecks` shape drifted cost the user the title, body, comments, reviewers and merge controls — everything they opened the sidebar for — over a section that renders a row of icons. Main never noticed because its unchecked reader answered `[]` for the same reply; this branch's reader refuses it, which is correct, and which is what makes the containment necessary. Contained the way phase 2 already is: a failed read keeps `kind: 'ready'`, empties `checks`, and carries the message in a new `checksError` so the checks section can say what happened. The sidebar can no longer reach `error` or `blocked` on the checks read alone. Also pins the enum departure this PR makes deliberately. The degrading arm sets go through `salvagedOptional(name, z.enum(...))` rather than `openEnum` because `openEnum` refuses a non-string where main mapped it to the conservative arm; nothing held that, and all 2477 tests stayed green against the swap. Six cases now hold both halves: a non-string degrades on the three open sets, and an unknown arm drops the row on the closed ones. Four deletions the reviewer found: a reaction-token alias with no importers, the `errorType`/`fetchedAt` the branch-lookup reader fabricated to satisfy a type whose only consumer reads neither, two bare schema aliases, and a quick-commands pass-through with two callers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
`--record` refuses unless the product tree equals `baseline`, so the fix above
moves the pin. The corpus re-recorded in place against it: 730 goldens, every
one header-only on `baseline`, no observation moved.
No observation moved because no family reaches the code the fix changed. The
`github.pr-read` family calls the seven wrapper reads directly and records
their `{ ok, error }` outcomes; `loadPrSidebarData` sits a layer above that and
no scenario mounts it. The prChecks outcome is identical before and after —
what changed is what the sidebar does with it — so the unit suite is the only
oracle for the containment.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
ℹ️ No critical issues — one minor suggestion inline.
Reviewed changes
Round 1 of review since the prior pullfrog review (6bc033abbe): the checks-containment fix and its in-place re-record.
- Contained a failed
github.prChecksread —loadPrSidebarDatanow returnsreadywithchecks: []and achecksErrorinstead of routing throughfailureState,PRChecksSectionrenders the message, andPrSidebarDatacarries the new required field. - Pinned the enum arms — six new schema cases: three assert a non-string degrades to main's fallback on the open sets (
status,mergeable,checksStatus), three assert a closed arm set drops the row or refuses the diff rather than defaulting. - Deleted dead surface —
GitHubReactionContentToken, thegithubPrChecksSchema/githubPrAssignableUsersSchemare-export aliases,PRRefreshOutcomeon the branch lookup (replaced by a localGitHubPrForBranchOutcomewithout the fabricatederrorType/fetchedAt), and thereadQuickCommandsindirection. - Repinned the corpus to
e5f601586band re-recorded in place (all goldens move onbaselinealone).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
The containment landed with no golden: no scenario mounted `loadPrSidebarData`, so the row in the delta table rested on unit tests alone. `PrSidebarLoadDeps` is five client-taking functions, so a new adapter drives phase 1 directly and records the `PrSidebarState` it resolves to — no React host, and no edit to an existing adapter, so no recorded golden moves. Two scenarios: a normal load, and one whose checks leg answers a shape the reader refuses. The matrix over the base then drives all eleven partitions at `github.prChecks#1`, and every one of them records `ready` with a `checksError` where main took the whole sidebar to `error`. `pr-sidebar-checks-failure-state` is the mutant that routes the refusal back through `failureState`; it moves both `pr-sidebar-checks-refused` and the prChecks matrix golden. Also pins two closed-and-required enum decisions that were free to become defaults — an unknown check-summary state drops the summary block, an unknown reaction content drops the reaction — deletes four exported type aliases and five enum constants with no reader outside their own file, makes `PRChecksSection`'s `checksError` required so a second caller cannot silently lose the message, and stops the header reading "No checks" when the checks were unreadable rather than absent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Six new goldens — two pilots and the four matrix sites the base scenario scripts — and `baseline` on the 730 that already existed. No body moved and no `adapterSha256`: the family is a new adapter module, so nothing recorded through another one re-digests. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The one earlier inline finding (explicit-
nullcollapse ongithub-pr-read-reply-schema.ts) remains open — it is on unchanged lines, so it is not re-raised here.
Reviewed changes
Round 2 of review since the prior pullfrog review (c8a0bbf65b): the session.pr-sidebar recording family and the checks-header polish.
- Recorded the checks containment — a new
session.pr-sidebaradapter drivesloadPrSidebarDatadirectly (no React host) over a normal load and a refused-checks load; all elevengithub.prChecks#1matrix partitions recordreadywith achecksError, and the newpr-sidebar-checks-failure-statemutant re-routes the refusal throughfailureState, so the family is proven able to fail. - Fixed the empty header wording — the section now renders
prChecksSummaryLabel(summary, checksError), readingChecks unavailablerather thanNo checkswhen the checks read failed;checksErroris a required prop, and the prior inline nit is retired. - Pinned two closed enum decisions — new schema cases assert an unknown check-summary state drops the summary block and an unknown reaction content drops the reaction.
- Deleted dead surface — four exported type aliases removed and five enum constants unexported, none with an external reader.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Every golden and the scenario manifest take main's fenced corpus: the 705 shared goldens are byte-identical to `origin/main` and the manifest keeps main's `baseline` (9add08b). The branch's 11 scenarios and 31 goldens ride along unrecorded here; the next commit repins and re-records them. `unchecked-rpc-reader-inventory.ts` auto-merged cleanly: the session block stays deleted, and main's raise of `mobile-workspace-create-operations` from four readers to five (#19850's `agent-launch-receipt`) survives. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Repins `baseline` to the merge commit and re-records all 736 goldens in place. Against `origin/main` the 705 shared goldens move only on `baseline` (672 of them header-only), leaving the same 33 body moves and the same partitions the branch carried before the merge, plus its 31 added goldens. Every body also takes main's recorder shape from #21088: `sent` becomes `ordinal` over one interleaved write counter, subscriptions record a cleanup checkpoint, and a salvaging read now reports a `reply-salvage` effect naming what it dropped. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
`autoMergeAllowed` and `mergeQueueRequired` carry three answers, not two: `null` is GitHub saying auto-merge is not allowed, `undefined` is the host not carrying the member at all. The readers coalesced the null away, so a well-formed reply read differently from the parsers they replaced, which preserved it explicitly. Both shared types already declare `boolean | null`. No consumer separates the two today — `pull-request-auto-merge-availability` compares with `=== true` and `!== false` — so this is parity, not a visible fix, which is exactly why it needed a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
All 736 goldens move on `baseline` alone: no scenario scripts an explicit null on either flag, so preserving it changes no recorded screen. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
✅ No new issues found. The earlier inline finding (explicit-
nullcollapse ongithub-pr-read-reply-schema.ts) is addressed ind8d8dddea5, and its thread is resolved.
Reviewed changes
Round 4 of review since the prior pullfrog review (a4c4ac44e7): the tri-state flag parity fix and its corpus repin.
- Preserved the host's explicit
nullonautoMergeAllowed(branch review and PR) andmergeQueueRequired— the three?? undefinedcollapses are gone, so a well-formed reply now reads exactly the way main'svalue.x === null ? null : ...parsers did. - Pinned the tri-state contract with two schema cases: an explicit
nullsurvives,falsesurvives, a non-boolean drops toundefined, and each flag's absence staysundefined. - Repinned all 736 goldens on
baselinealone plus the manifest — no scenario scripts anullon either flag, so no recorded screen moves.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Integration only, no behaviour: #21083's two new session-write readers (`created-terminal-tab`, `terminal-display-mode-set`) come in exactly as main wrote them, and the inventory carries the line for them under this file's own merge rule. The next commit converts both and the session block is empty again. Goldens and the manifest take main's bytes for every path main has — 727 goldens and all 368 scenarios byte-identical, `baseline` left at main's 9add08b — with this branch's 31 goldens and 11 scenarios beside them. Hand-resolved besides the inventory: `mobile-session-write-operations.ts` keeps both sides' readers, `operation-mutations.ts` unions both mutant sets, and `mobile-session-route-parity.test.ts` unions the two refresh notes and repins all three body digests over the merged source. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Step 7 empties the session block of the unchecked-reader inventory, and #21083 landed two readers into it after that: the New Tab create's member read of `tab`, and the display-mode toggle's payload. Converting them is what keeps the claim true — a session line reappearing would mean the domain is not migrated. `created-terminal-tab` requires `tab.id` and `tab.type === 'terminal'`, because the strip keys the new tab on the id and spreads the rest into a union whose arm `type` picks. `terminal`, `title` and `terminalTheme` stay optional behind main's own guards, and unknown members pass through. `terminal-display-mode-set` reads nothing, so it takes the same `z.unknown()` the other five unread writes take. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
All 736 goldens this branch already had move on `baseline` alone, and #21083's 22 arrive beside them. One of the 22 moves against main's own recording: `matrix-session.create-terminal-session.tabs.createterminal-1`, where the New Tab create's five malformed partitions read `Cannot read properties of undefined (reading 'tab')` and now read the method's own message. Two of them also stop unsubscribing the terminal the user was watching before the property read threw, so a create that never happened no longer costs the live pane its subscription. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
ℹ️ No critical issues — one stale mechanism comment inline.
Reviewed changes
Round 5 of review since the prior pullfrog review (135e5d959d): the second origin/main merge, the two session-write readers that merge brought, and the repin.
- Merged
origin/maina second time for #21083 — integration only; every file main changed is taken as main wrote it, and the branch's own scenarios and session block are untouched by the merge. - Converted the two session writers #21083 re-added —
sessionTabCreateTerminalnow readssessionCreatedTerminalTabSchema(requirestab.type === 'terminal'and a non-emptytab.id;terminal/title/terminalThemesalvage-optional; loose sostatusand newer members pass) andterminalDisplayModeSettakes the unreadz.unknown()schema, so the session line leaves the inventory again. - Pinned the new schema with two unit cases (id/type refusals; null kept, non-string salvaged, passthrough) and re-recorded the create family's five malformed partitions to the method-named message, two of which keep the live pane's subscription that main dropped before the property read threw.
- Repinned
baselineand re-recorded — all 736 pre-existing goldens move onbaselinealone, #21083's 22 arrive, and exactly one golden body-moves vs main's own recording.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…7) (stablyai#21089) * test(mobile): record main's session reply behaviour at every unrecorded read site Step 7 for the session domain changes how 51 RPC readers read a *malformed* reply. Eleven of the session read sites had no recording family, so main's answer to a malformed reply at those sites was undocumented and the reader change would have had nothing to move. This commit is the before picture, taken from main's own tree with no product edit in it. Ten new families, twelve scenarios, twenty-five goldens: - `session.review-file-diff` / `session.review-branch-diff` — `git.diff` and `git.branchDiff` read through the review projection, which the Changes screen's verbatim readers do not cover. - `session.review-git-mutations` — the single-file `git.stage` / `git.discard` and the bulk stage sweep's second `git.stage`. - `session.review-send-sheet` — `session.tabs.list` read for the agent terminals the send sheet lists, the third reader on that method. Needs an `open-send-sheet` action on the review-action adapter, which re-digests that family's eight goldens on `adapterSha256` and nothing else. - `session.browser-tab-create` — `browser.tabCreate`. - `agentSession.structured-create` — `agentSession.create`, whose family base only ever covered the support probe. - `session.tab-rename` / `session.tab-close-session` — `terminal.rename` and `session.tabs.close`. - `settings.new-tab-local-agents` — `preflight.detectAgents`, the arm the new-tab loader takes for a workspace with no connection. `baseline` is repinned to main's tip because two commits (stablyai#20659, stablyai#21004) touched a fenced path after the pilot's pin, so `--record` refuses on main's own tree until it moves. The repin is what rewrites `baseline` on all 705 existing goldens; nothing else about them moves. Decoded against origin/main through the value pool: 705 header-only (`baseline` on every one, `adapterSha256` on the eight review-action goldens), 0 body-moved, 25 added, 0 deleted. Not covered, with the reason: the chunked clipboard upload's `appendImageUploadChunk`, `commitImageUpload` and `abortImageUpload` cannot be matrixed, because `replyMatrixSites` takes every completion in the base scenario and the chain's later params carry the `uploadId` the start reply named. Driving `clipboard.startImageUpload#1` therefore makes main send an append whose params no scripted step matches, and the recorder raises `Request params mismatch: clipboard.appendImageUploadChunk#1` instead of recording. The two families were written, probed and removed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): checked reply readers for the session domain (step 7) Fifty-one unchecked reply readers across nine files become checked zod readers, so a malformed host reply surfaces as one readable error at the operation boundary instead of a downstream `TypeError`, a rendered `undefined`, or a screen left ready over garbage. Deliberately a behaviour change on malformed replies only. Eight schema modules, one per reply family, each recording the consumer line behind every requirement and the host handler that publishes it: - `clipboard-image-reply-schema.ts` — the upload slot's `uploadId`, the commit and single-frame path strings, and the two legs whose body nothing reads. - `github-pr-mutation-reply-schema.ts` — the `{ ok, error }` status envelope as two variants, and the bare-boolean confirmation. - `github-pr-entity-reply-schema.ts` / `github-pr-read-reply-schema.ts` — the seven PR sidebar reads. Every identity requirement the hand parsers had is kept, so a payload that degraded to null still degrades to null; what changes is a payload that is not the declared container at all. - `diff-review-reply-schema.ts` — the normalized branch compare, the review notes on the worktree record, the three file-diff arms, and the file-level git mutations. - `review-terminal-reply-schema.ts`, `session-launch-reply-schema.ts`, `session-read-reply-schema.ts`, `session-write-reply-schema.ts` — the review send sheet, the launch paths, the session screen's reads and its writes. Requirements are exactly the members a consumer reads unguarded, everything else is a salvaged optional with main's own default applied in the transform, and no schema is `.strict()`: a member a newer host adds passes through untouched. Enum arm sets that a reader compares against pass through or degrade to the arm the reader handles most conservatively; the two closed sets — the committed change status and the diff kind — are closed because main *dropped* an arm it did not know rather than passing it through, and degrading them would draw a row or render a diff main never did. No member is coerced on the way back to the host. `github-pr-parsers.ts`, `github-pr-comment-parsers.ts` and `github-pr-value-readers.ts` are gone; their suite is now the parity record for the schemas that replaced them, with the four cases that refuse rather than degrade marked as such. Twelve call-site casts are deleted, and three dead "response was invalid" branches with them: the reader refuses those replies now, so the error names its method. The nine session files come off `unchecked-rpc-reader-inventory.ts` entirely rather than being lowered. `git show --stat` on this commit touches nothing under `mobile/rpc-foundation`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): unit-pin every session reply schema's decision Three kinds of case, one per kind of decision the schemas encode: a member a consumer reads unguarded is required and its absence refuses, an arm set a reader compares against degrades to the arm that reader handles most conservatively, and a reply whose arms need different members is declared as variants and each arm is read. The last suite is the wire-compatibility claim: a member no reader knows passes straight through, on the markdown document, the upload slot and the terminal inventory alike, so a newer host is never refused for a field mobile does not read. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh the corpus for the session domain's checked readers Repins `baseline` to the last commit touching a fenced path and re-records all 730 goldens, which is the disclosed behaviour change taken as an observation. Decoded through the value pool against the pre-refactor tree on this branch: 688 header-only with `baseline` the only key that moved, 42 body-moved, 0 added, 0 deleted. The 42 are seven named scenarios and thirty-five matrix goldens, and every moved checkpoint's own reply is malformed or refused. Three `normal` partitions appear in the list and none of them reads a well-formed reply differently: the review file-diff family's base scenario drives three legs and its third is scripted `{ kind: 'unknown' }`, so that leg's checkpoint moves in every variant, the varied leg included. The same append-only-history effect puts `pr-read-upstream-error`'s `no-pr` checkpoint in the list for the malformed PR recorded before it. What the corpus now records, in one sentence: a property read on null, a V8 destructuring message shown to the user, and four hand-written "response was invalid" strings are replaced by one message that names the method, and four screens that published a malformed payload as ready state now show an error instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): split the expanded check run out of the PR read schemas `github-pr-read-reply-schema.ts` was 328 code lines against the 300-line cap. The expanded check run and the annotations, jobs and steps listed under it are one reply with no reader in common with the other six, so they move to `github-pr-check-reply-schema.ts` whole. A move, not an edit: no schema changes and no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the branch's last fenced-path commit The schema-module split touched `mobile/src`, so `--record` refuses on the pin the previous refresh left behind. Repins to that commit and re-records. Decoded against the previous corpus: 730 header-only with `baseline` the only key that moved, 0 body-moved, 0 added, 0 deleted — the split is a move, and the corpus says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the worktree display-name cast's type import The live-title read is typed by its schema now, so the cast it annotated is gone and the import it needed with it. oxlint flags the leftover. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the branch tip The unused-import removal touched a fenced path, so the pin moves with it. Decoded against the previous corpus: 730 header-only on `baseline` alone, 0 body-moved, 0 added, 0 deleted. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): contain a refused prChecks reply to the checks section The checks read was the one phase-1 dependency that could take the whole PR sidebar down. `loadPrSidebarData` routed `!checksOutcome.ok` through `failureState`, so a host whose `github.prChecks` shape drifted cost the user the title, body, comments, reviewers and merge controls — everything they opened the sidebar for — over a section that renders a row of icons. Main never noticed because its unchecked reader answered `[]` for the same reply; this branch's reader refuses it, which is correct, and which is what makes the containment necessary. Contained the way phase 2 already is: a failed read keeps `kind: 'ready'`, empties `checks`, and carries the message in a new `checksError` so the checks section can say what happened. The sidebar can no longer reach `error` or `blocked` on the checks read alone. Also pins the enum departure this PR makes deliberately. The degrading arm sets go through `salvagedOptional(name, z.enum(...))` rather than `openEnum` because `openEnum` refuses a non-string where main mapped it to the conservative arm; nothing held that, and all 2477 tests stayed green against the swap. Six cases now hold both halves: a non-string degrades on the three open sets, and an unknown arm drops the row on the closed ones. Four deletions the reviewer found: a reaction-token alias with no importers, the `errorType`/`fetchedAt` the branch-lookup reader fabricated to satisfy a type whose only consumer reads neither, two bare schema aliases, and a quick-commands pass-through with two callers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the containment commit `--record` refuses unless the product tree equals `baseline`, so the fix above moves the pin. The corpus re-recorded in place against it: 730 goldens, every one header-only on `baseline`, no observation moved. No observation moved because no family reaches the code the fix changed. The `github.pr-read` family calls the seven wrapper reads directly and records their `{ ok, error }` outcomes; `loadPrSidebarData` sits a layer above that and no scenario mounts it. The prChecks outcome is identical before and after — what changed is what the sidebar does with it — so the unit suite is the only oracle for the containment. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the PR sidebar's checks containment The containment landed with no golden: no scenario mounted `loadPrSidebarData`, so the row in the delta table rested on unit tests alone. `PrSidebarLoadDeps` is five client-taking functions, so a new adapter drives phase 1 directly and records the `PrSidebarState` it resolves to — no React host, and no edit to an existing adapter, so no recorded golden moves. Two scenarios: a normal load, and one whose checks leg answers a shape the reader refuses. The matrix over the base then drives all eleven partitions at `github.prChecks#1`, and every one of them records `ready` with a `checksError` where main took the whole sidebar to `error`. `pr-sidebar-checks-failure-state` is the mutant that routes the refusal back through `failureState`; it moves both `pr-sidebar-checks-refused` and the prChecks matrix golden. Also pins two closed-and-required enum decisions that were free to become defaults — an unknown check-summary state drops the summary block, an unknown reaction content drops the reaction — deletes four exported type aliases and five enum constants with no reader outside their own file, makes `PRChecksSection`'s `checksError` required so a second caller cannot silently lose the message, and stops the header reading "No checks" when the checks were unreadable rather than absent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the pr-sidebar family commit Six new goldens — two pilots and the four matrix sites the base scenario scripts — and `baseline` on the 730 that already existed. No body moved and no `adapterSha256`: the family is a new adapter module, so nothing recorded through another one re-digests. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the corpus against the merged main Repins `baseline` to the merge commit and re-records all 736 goldens in place. Against `origin/main` the 705 shared goldens move only on `baseline` (672 of them header-only), leaving the same 33 body moves and the same partitions the branch carried before the merge, plus its 31 added goldens. Every body also takes main's recorder shape from stablyai#21088: `sent` becomes `ordinal` over one interleaved write counter, subscriptions record a cleanup checkpoint, and a salvaging read now reports a `reply-salvage` effect naming what it dropped. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep an explicit null on the two tri-state PR flags `autoMergeAllowed` and `mergeQueueRequired` carry three answers, not two: `null` is GitHub saying auto-merge is not allowed, `undefined` is the host not carrying the member at all. The readers coalesced the null away, so a well-formed reply read differently from the parsers they replaced, which preserved it explicitly. Both shared types already declare `boolean | null`. No consumer separates the two today — `pull-request-auto-merge-availability` compares with `=== true` and `!== false` — so this is parity, not a visible fix, which is exactly why it needed a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the tri-state flag commit All 736 goldens move on `baseline` alone: no scenario scripts an explicit null on either flag, so preserving it changes no recorded screen. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): check the two session-write readers stablyai#21083 brought Step 7 empties the session block of the unchecked-reader inventory, and stablyai#21083 landed two readers into it after that: the New Tab create's member read of `tab`, and the display-mode toggle's payload. Converting them is what keeps the claim true — a session line reappearing would mean the domain is not migrated. `created-terminal-tab` requires `tab.id` and `tab.type === 'terminal'`, because the strip keys the new tab on the id and spreads the rest into a union whose arm `type` picks. `terminal`, `title` and `terminalTheme` stay optional behind main's own guards, and unknown members pass through. `terminal-display-mode-set` reads nothing, so it takes the same `z.unknown()` the other five unread writes take. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin and re-record over stablyai#21083's corpus All 736 goldens this branch already had move on `baseline` alone, and stablyai#21083's 22 arrive beside them. One of the 22 moves against main's own recording: `matrix-session.create-terminal-session.tabs.createterminal-1`, where the New Tab create's five malformed partitions read `Cannot read properties of undefined (reading 'tab')` and now read the method's own message. Two of them also stop unsubscribing the terminal the user was watching before the property read threw, so a create that never happened no longer costs the live pane its subscription. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say what carries a refused create reply to the catch Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

Step 7, session domain: 53 unchecked reply readers across 9 files become checked zod readers, so a malformed host reply surfaces as one readable error at the operation boundary instead of a downstream
TypeError, a V8 destructuring message shown to the user, or a screen left ready over garbage. Deliberately a behaviour change on malformed replies only; every well-formed recording is byte-identical. Needs explicit authorization to land (not under the refactor-series standing authorization).Diff composition (head
0d425031ee, vsorigin/mainatccb4d2044b)The branch has merged
origin/maintwice, at84cf72d01eand ate1829b9d4c, so the merge-base is the second merge and every number below is a two-dot diff against main's tip.baselinerepinned to the last fenced-path commit;adapterSha256on the 8 review-action goldens, the one adapter this branch edits); 34 body-moved (the delta); 31 added (eleven new families for the read sites main never recorded)session.pr-sidebaradapter and its register line, its mutant and the mutant's registry line, anopen-send-sheetaction on the review-action adapter, and the new-tab family list in the header-digest testThe delta, complete (45 goldens: 34 that existed on main, 9 of the new read-site families, and the 2
session.pr-sidebargoldens the containment moves)Before column is main's own recording, taken at
66bcea171ewith no product edit in the tree and re-read after the merge fromorigin/main's own corpus; after column is the branch tip. Every moved checkpoint's own reply is malformed or refused.github.pr-read/github.repoSlug#1null, so the sidebar reported "no GitHub remote" and hid the PR-view icon; outcomeok: trueok: falsewithThe host sent a reply this app could not read (github.repoSlug)github.pr-read/hostedReview.forBranch#1null— "no hosted review"ok: falsegithub.pr-read/github.prForBranch#1GitHub returned an invalid pull request response.… (github.prForBranch)github.pr-read/github.workItemDetails#1null— empty detail paneok: falsegithub.pr-read/github.prChecks#1, andsession.pr-sidebar/github.prChecks#1(new)matrix-session.pr-sidebar-github.prchecks-1recordsreadywith achecksErroron every one of the eleven partitions, so the sidebar never errors on checks alonegithub.pr-read/github.prCheckDetails#1nullok: falsegithub.pr-read/github.listAssignableUsers#1[]— an empty pickerok: falsegithub.pr-comment-mutation/github.resolveReviewThread#1+pr-comment-resolve-unconfirmedFailed to update review thread.for a reply that was not a boolean at all… (github.resolveReviewThread)github.pr-title-mutation/github.updatePRTitle#1Failed to update title.… (github.updatePRTitle)clipboard.image-upload+clipboard.image-attachment+nativeChat.image-upload/clipboard.startImageUpload#1Cannot destructure property 'uploadId' of 'startResponse.result' as it is undefined.shown in the composer, then a chunk append sent against an undefined slot… (clipboard.startImageUpload); the chain stops, and these three are the only goldens whose sender count moves at all (16 → 13)clipboard.image-upload/clipboard.saveImageAsTempFile#1session.pr-triage+aiVault.resume-launch/session.tabs.createTerminal#1, andpr-triage-invalid-terminal+aivault-resume-launch-invalid-tabCreated terminal response was invalid… (session.tabs.createTerminal)session.pr-triage+aiVault.resume-launch/terminal.send#1session.diff-review/git.branchCompare#1+diff-review-branch-compareCommitted changes response was invalid… (git.branchCompare)session.diff-review/review-show(worktree.show)… (worktree.show)session.review-file-diff/git.diff#1..#3anddiff-review-worktree-file-diff,review-file-diff-shapes(new){ kind: 'unknown' }Diff response was invalid… (git.diff)session.review-branch-diff/git.branchDiff#1(new)Diff response was invalid… (git.branchDiff)session.review-send-sheet/session.tabs.list#1(new)… (session.tabs.list)session.tab-documents/markdown.readTab#1readywithcontent: undefinedand an undefined base versionCouldn't load markdownsession.markdown-save/markdown.saveTab#1content: undefined, cleared the dirty flag and raised a "Saved" toastversion: v1and the user's unsaved textsession.diff-notes/worktree.show#1TypeErrorreadingworktreeRpcIncompatibleReplyError,code: incompatible_replysession.worktree-connection+tasks.route-repo-list+settings-agent-read+components.new-workspace-repositories+settings.new-tab-local-agents/repo.list#1Cannot read properties of undefined (reading 'repos'), andworktree_repo_not_foundfor a reply whosereposwas an error envelope… (repo.list)settings-agent-read/preflight.detectRemoteAgents#1,settings.new-tab-local-agents/preflight.detectAgents#1(new)detectedAgents is not iterable… (preflight.detectAgents)/(preflight.detectRemoteAgents)session.browser-tab-create/browser.tabCreate#1(new)Cannot read properties of undefined (reading 'browserPageId')… (browser.tabCreate)session.create-terminal/session.tabs.createTerminal#1(#21083's family)Cannot read properties of undefined (reading 'tab')in the New Tab error row, and on two partitions main had already unsubscribed the terminal the user was watching before the property read threw… (session.tabs.createTerminal), and the live pane keeps its subscription because the refusal lands before the teardownThree
normalpartitions appear in the moved set and none of them reads a well-formed reply differently. The review file-diff family's base scenario drives three legs and its third is scripted{ kind: 'unknown' }, so that leg's checkpoint moves inside every variant, thenormalone included. The same append-only-history effect putspr-read-upstream-error'sno-prcheckpoint in the list, for the malformed PR recorded before it.Contract
66bcea171erecords main's behaviour at the eleven session read sites that had no family, from main's own tree, before any product edit.4eba48bfe0is the readers;git show --staton it touches nothing undermobile/rpc-foundation. Then the schema unit suite, the refresh, a module split forced by the 300-line cap, and the repins that follow the fence. Round 1 of review addse5f601586b(the containment, the enum pins and four deletions) andc8a0bbf65b(its repin). Round 2 adds0029646a5b(thesession.pr-sidebarfamily, two more enum pins, four dead type aliases deleted and five enum constants unexported) anda4c4ac44e7(its repin and the in-place re-record each repin requires).84cf72d01ethen mergesorigin/mainonce — every shared golden and the manifest'sbaselinetaken from main, nothing spliced — and77d34a8e74repins to the merge and re-records all 736 in place.d8d8dddea5is the tri-state flag fix below, with135e5d959dits repin.e1829b9d4cmergesorigin/maina second time for refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083, integration only;ccd228af24converts the two session-write readers that PR landed after this branch emptied the block;0d425031eerepins and re-records..strict(). Variants where the host's own result is a union: the PR mutation status envelope against a void reply, the three file-diff arms, and the branch lookup's four outcome arms.salvagedOptional(name, z.enum(...))plus main's own fallback rather thanopenEnum. The difference is deliberate and is the one place this PR departs from the pilot's letter:openEnummakes a non-string fatal, and main's readers mapped a number or an object to the same conservative arm a bad string got (readCheckStatus→pending,readMergeableState→UNKNOWN, the compare status →error). UsingopenEnumwould refuse a PR row main rendered, which is a behaviour change on a member outside this PR's scope. Two sets stay closed — the committed-change status and the diff kind — because main dropped an arm it did not know rather than passing it through, so degrading them would draw a row or render a diff main never did. Six cases hold both halves, because nothing did: three assert a non-string degrades to main's fallback on the open sets (the swap toopenEnum(..., fallback).optional()passes all 2477 other tests and flips exactly these three), and five assert an unknown arm drops the check row, drops the committed change, drops the checks summary block, drops the reaction, or refuses the diff rather than defaulting it.loadPrSidebarDataused to route a refusedgithub.prChecksthrough the same classifier a missing GitHub account goes through, which would have cost the user the title, body, comments, reviewers and merge controls over a row of icons — a regression this PR would have introduced, since main's unchecked reader answered[]for the same reply. The refusal is now carried inchecksErrorand rendered in the checks section, and the sidebar cannot reacherrororblockedon the checks read alone.session.pr-sidebaris the family that records it: the adapter drivesloadPrSidebarDataagainst the scripted client, so the recorded state is thePrSidebarStatethe user's screen is built from.provideris only ever compared to'github'(mobile-pr-sidebar-state.ts:91) and never forwarded, and the repo slug that is forwarded isowner/repo/hoststrings with no enum anywhere in the path.baselineis the branch's last commit touching a fenced path (ccd228af24), which is what--recordrequires. After a squash-merge that sha is unreachable until a follow-up main repin, the same two-step fix(mobile): two known main bugs the RPC migration preserved #20563 → fix(lint): merge duplicate type imports in the mobile RPC recorder adapters #20895 used.Evidence
origin/mainafter the second merge: 693 header-only (baselineon all 727 shared,adapterSha256on 8), 34 body-moved, 31 added, 0 deleted. Thirty-three are the same goldens with the same moved partitions the branch carried before either merge. The thirty-fourth is refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083's own create family, moved by the reader conversion above. Against the pre-merge head135e5d959dall 736 shared goldens move onbaselinealone with no body move at all, and refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083's 22 arrive beside them, so the second merge imported main's corpus without disturbing anything already recorded. Against the pre-merge heada4c4ac44e7every body moves, all of it main's: test(mobile): recorder follow-ups — write ordinal, teardown streams, context anchor, salvage observation, provider pass-through, React draw #21088 renamed the recorder'ssentto an interleavedordinal, gave subscriptions a cleanup checkpoint, and made a salvaging read publish areply-salvageeffect. Ignoring the write counter, eleven goldens move for a reason: five carry fix(mobile): open native chat when a new worktree launches a default agent #19850's newagentLaunchcapability (one of them,matrix-worktree.create-retry-worktree.create-1, now reportsFailed to create workspacewhere main crashed with aTypeError), four are the subscription cleanup checkpoints, and two are this branch's send-sheet goldens picking up the newreply-salvageeffect, which names the one dropped tab. Decode, readers vs the before recording: 688 header-only onbaselinealone, 42 body-moved, 0 added, 0 deleted. Decode, each repin alone: 730 header-only onbaseline, 0 body-moved — the module split, the unused-import removal and the containment fix move no observation. Decode, round 2 vsc8a0bbf65b: 730 header-only onbaseline, 0 body-moved, 6 added, noadapterSha256anywhere, because a new adapter module re-digests nothing recorded through another one.session.pr-sidebarmounts phase 1 itself —PrSidebarLoadDepsis five client-taking functions, so the adapter callsloadPrSidebarDatawith no React host — over two scenarios: a normal load, and one whose checks leg answers a shape the reader refuses. Six goldens: the two pilots and the four matrix sites the base scripts. Atgithub.prChecks#1all eleven partitions recordkind: readywith the refusal inchecksError, where main took the sidebar toerrororblocked.pr-sidebar-checks-failure-state, the mutant that routes!checksOutcome.okback throughfailureState, failspr-sidebar-checks-refusedandmatrix-session.pr-sidebar-github.prchecks-1.openEnumpair again after the second, and all still fail the same tests: the containment'sfailureState(5 failures — two sidebar unit cases,pr-sidebar-checks-refused,matrix-session.pr-sidebar-github.prchecks-1, and the registered mutant test), theopenEnumswap (3), and the check-summary.catch(1):tabmade a salvaged optional again failsmatrix-session.pr-triage-session.tabs.createterminal-1atpr-triage-launch.inner-ok-missing:launched(settlements.launch.error.categoryexpectedError, actualTypeError), and fails the unit case that requires a created terminal to name its handle.rpcUncheckedMemberReaderfailsmatrix-session.worktree-connection-repo.list-1atterminal-worktree-connection-resolved.result-absent:resolved(expectedRpcIncompatibleReplyError, actualTypeError) and trips the ratchet:A new operation validates its reply with rpcResultVariant. Nothing may be added to unchecked-rpc-reader-inventory.ts.openEnum(..., fallback).optional()fails exactly the three new cases —degrades a non-string check status to pending rather than refusing the review,… mergeable state to UNKNOWN …,… PR checksStatus to pending …— and nothing else across the suite's 5,766.statusmade a salvaged optional defaulting to'queued'failsdrops a check row whose status is an arm the icon cannot draw(['ci','drift','lint']where the row must leave), and nothing else.stateand the reactioncontenteach given.catch(...)— the swap from drop-the-block to degrade-to-default — fail exactlydrops a checks summary whose state is an arm the header cannot colouranddrops a reaction whose content is an arm with no emoji to draw, one test each across the full 5,766, and move no golden. That is why they are pinned: main'sreadCheckSummary/readReactionreturnedundefined/nullfor an unknown arm, and nothing else would have caught the change.z.strictObjecton the markdown tab document rejects a newer host's reply —unrecognized_keys: ["collaborators", "revisionKind"]— and is caught only by the unit suite. No golden moves, because no scenario scripts a member mobile does not read; that suite is the oracle for this class, exactly as the pilot found.created-terminal-tabandterminal-display-mode-setinto a session file after this branch had emptied that block of the inventory. The merge takes them as main wrote them and the commit after it converts both, because a session line reappearing would mean the domain is not migrated. Only the create moves a golden: the display-mode toggle reads no verdict, soz.unknown()behind it changes nothing, which is what refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083 measured and this re-record confirms.autoMergeAllowedandmergeQueueRequiredcarry three answers:nullis GitHub saying auto-merge is not allowed,undefinedis the host not carrying the member. The readers coalesced the null away with?? undefined, which read a well-formed reply differently fromreadHostedReview/readPRForBranch, which preserved it explicitly, and from the shared types, which declareboolean | nullon both. Dropped at all three sites. No consumer separates the two today (pull-request-auto-merge-availability.tscompares with=== trueand!== false), and no scenario scripts a null on either flag, so the repin moves all 736 goldens onbaselinealone and no body — which is why the two new cases,keeps a null autoMergeAllowed on the branch review and drops a non-booleanandkeeps a null autoMergeAllowed and mergeQueueRequired on the PR and drops a non-boolean, are the only oracle. Restoring either?? undefinedfails exactly those two.github-pr-parsers.ts,github-pr-comment-parsers.tsandgithub-pr-value-readers.tsare gone. Their suite now runs the schemas through the same 39 expectations — the nestedauthor.loginreviewer shape, the droppedprRepo, theaction_requiredconclusion, the salvaged rows — with four cases rewritten to assert a refusal where main degraded.Not covered, with the reason
The chunked clipboard upload's
appendImageUploadChunk,commitImageUploadandabortImageUploadhave no reply matrix.replyMatrixSitestakes every completion in a family's base scenario, and this chain's later params carry theuploadIdthe start reply named, so drivingclipboard.startImageUpload#1makes main send an append whose params no scripted step matches. The recorder raisesRequest params mismatch: clipboard.appendImageUploadChunk#1and records nothing. Both families were written, probed and removed; the three readers are pinned by the schema unit suite and by the existing well-formedclipboard-image-upload-chunkedandclipboard-image-attachment-pastedgoldens, which do exercise append and commit.Gates
ORCA_BACKGROUND_LAUNCH=1: rootpnpm tc0; mobilepnpm run typecheckfrommobile/0; full mobile suite 602 files / 5,824 passed / 6 skipped;check:code-quality:changed0 across 54 files (all six sub-gates);format:check0 over 2,662 files undermobile/; oxlint 0 on the changed TypeScript files;check:max-lines-ratchetandcheck:readme-local-links0. No file outsidemobile/changed.