Skip to content

refactor(mobile): checked reply readers for the session domain (step 7) - #21089

Merged
Jinwoo-H merged 20 commits into
mainfrom
step7-session-readers
Sep 17, 2026
Merged

Jinwoo-H merged 20 commits into
mainfrom
step7-session-readers

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​636 $\color{#cf222e}{\Huge{\mathbf{−}}}$​84 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​552
Prod 809 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​27937 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4624 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​23313

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, vs origin/main at ccb4d2044b)

The branch has merged origin/main twice, at 84cf72d01e and at e1829b9d4c, so the merge-base is the second merge and every number below is a two-dot diff against main's tip.

Part Files Added Deleted What it is
Goldens 758 +25,665 -3,535 693 header-only (baseline repinned to the last fenced-path commit; adapterSha256 on 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)
Scenario manifest 1 +667 -1 11 new scenarios + the repin, over main's 368
Product source 46 +1,531 -1,088 9 schema modules, 51 readers converted, 15 call-site casts removed against 1 added and 5 dead "response was invalid" branches deleted, the session block removed from the unchecked-reader inventory, and three hand-written parser modules replaced by schemas
Tests 8 +630 -83 the schema unit suite, the PR parser suite rewritten as the schemas' parity record, the sidebar-containment cases, the tri-state flag cases, the created-tab cases, two error-copy expectations, three route body pins
Recorder + adapters 6 +80 -1 the session.pr-sidebar adapter and its register line, its mutant and the mutant's registry line, an open-send-sheet action on the review-action adapter, and the new-tab family list in the header-digest test

The delta, complete (45 goldens: 34 that existed on main, 9 of the new read-site families, and the 2 session.pr-sidebar goldens the containment moves)

Before column is main's own recording, taken at 66bcea171e with no product edit in the tree and re-read after the merge from origin/main's own corpus; after column is the branch tip. Every moved checkpoint's own reply is malformed or refused.

Family / site Partitions Main (before) This PR (after)
github.pr-read / github.repoSlug#1 result-absent, inner-* (4) slug read as null, so the sidebar reported "no GitHub remote" and hid the PR-view icon; outcome ok: true ok: false with The host sent a reply this app could not read (github.repoSlug)
github.pr-read / hostedReview.forBranch#1 result-absent review read as null — "no hosted review" readable message, ok: false
github.pr-read / github.prForBranch#1 result-absent, inner-* GitHub returned an invalid pull request response. … (github.prForBranch)
github.pr-read / github.workItemDetails#1 result-absent, inner-* details read as null — empty detail pane readable message, ok: false
github.pr-read / github.prChecks#1, and session.pr-sidebar / github.prChecks#1 (new) all 5 at the wrapper; all 11 at the sidebar the PR rendered with an empty checks row — "no checks" on a PR that has them the PR still renders, with a readable message in the checks section; matrix-session.pr-sidebar-github.prchecks-1 records ready with a checksError on every one of the eleven partitions, so the sidebar never errors on checks alone
github.pr-read / github.prCheckDetails#1 result-absent run details read as null readable message, ok: false
github.pr-read / github.listAssignableUsers#1 all 5 reviewers read as [] — an empty picker readable message, ok: false
github.pr-comment-mutation / github.resolveReviewThread#1 + pr-comment-resolve-unconfirmed all 5 + the named scenario Failed to update review thread. for a reply that was not a boolean at all … (github.resolveReviewThread)
github.pr-title-mutation / github.updatePRTitle#1 all 5 Failed to update title. … (github.updatePRTitle)
clipboard.image-upload + clipboard.image-attachment + nativeChat.image-upload / clipboard.startImageUpload#1 all 5 Cannot 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#1 all 5 the single-frame fallback returned a non-string as the image path readable message; the upload rejects
session.pr-triage + aiVault.resume-launch / session.tabs.createTerminal#1, and pr-triage-invalid-terminal + aivault-resume-launch-invalid-tab all 5 + two named scenarios Created terminal response was invalid … (session.tabs.createTerminal)
session.pr-triage + aiVault.resume-launch / terminal.send#1 result-absent, result-null send read as accepted, prompt reported sent rejects with the readable message
session.diff-review / git.branchCompare#1 + diff-review-branch-compare all 5 + the named scenario Committed changes response was invalid … (git.branchCompare)
session.diff-review / review-show (worktree.show) result-absent, result-null review notes read as two absent members rejects with … (worktree.show)
session.review-file-diff / git.diff#1..#3 and diff-review-worktree-file-diff, review-file-diff-shapes (new) all 11 at the varied leg; the base's third leg is { kind: 'unknown' } Diff response was invalid … (git.diff)
session.review-branch-diff / git.branchDiff#1 (new) all 5 Diff response was invalid … (git.branchDiff)
session.review-send-sheet / session.tabs.list#1 (new) all 5 sheet opened empty — "this worktree has no agent sessions" … (session.tabs.list)
session.tab-documents / markdown.readTab#1 inner-* (3) markdown tab published ready with content: undefined and an undefined base version error state, Couldn't load markdown
session.markdown-save / markdown.saveTab#1 all 5 on three of them main wiped the document to content: undefined, cleared the dirty flag and raised a "Saved" toast the save rejects with the readable message, the toast is gone, and the doc keeps its loaded version: v1 and the user's unsaved text
session.diff-notes / worktree.show#1 result-absent, result-null TypeError reading worktree RpcIncompatibleReplyError, code: incompatible_reply
session.worktree-connection + tasks.route-repo-list + settings-agent-read + components.new-workspace-repositories + settings.new-tab-local-agents / repo.list#1 all 5 (3 where the screen already swallowed it) Cannot read properties of undefined (reading 'repos'), and worktree_repo_not_found for a reply whose repos was an error envelope … (repo.list)
settings-agent-read / preflight.detectRemoteAgents#1, settings.new-tab-local-agents / preflight.detectAgents#1 (new) all 5 detectedAgents is not iterable … (preflight.detectAgents) / (preflight.detectRemoteAgents)
session.browser-tab-create / browser.tabCreate#1 (new) result-absent, result-null Cannot read properties of undefined (reading 'browserPageId') … (browser.tabCreate)
session.create-terminal / session.tabs.createTerminal#1 (#21083's family) all 5 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 teardown

Three normal partitions 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, the normal one 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.

Contract

  • Commit order. 66bcea171e records main's behaviour at the eleven session read sites that had no family, from main's own tree, before any product edit. 4eba48bfe0 is the readers; git show --stat on it touches nothing under mobile/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 adds e5f601586b (the containment, the enum pins and four deletions) and c8a0bbf65b (its repin). Round 2 adds 0029646a5b (the session.pr-sidebar family, two more enum pins, four dead type aliases deleted and five enum constants unexported) and a4c4ac44e7 (its repin and the in-place re-record each repin requires). 84cf72d01e then merges origin/main once — every shared golden and the manifest's baseline taken from main, nothing spliced — and 77d34a8e74 repins to the merge and re-records all 736 in place. d8d8dddea5 is the tri-state flag fix below, with 135e5d959d its repin. e1829b9d4c merges origin/main a second time for refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083, integration only; ccd228af24 converts the two session-write readers that PR landed after this branch emptied the block; 0d425031ee repins and re-records.
  • Schema rules. Each schema requires exactly the members its consumer reads unguarded, with the consumer line and the host handler cited per member; everything else is a salvaged optional carrying main's own default, because the shared types declare those members non-optional and every renderer reads them unguarded. Nothing is .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.
  • Enum arm sets. The sets a reader compares against are open, but through salvagedOptional(name, z.enum(...)) plus main's own fallback rather than openEnum. The difference is deliberate and is the one place this PR departs from the pilot's letter: openEnum makes 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). Using openEnum would 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 to openEnum(..., 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.
  • Containment. Checks are the one phase-1 read whose failure is not the sidebar's failure. loadPrSidebarData used to route a refused github.prChecks through 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 in checksError and rendered in the checks section, and the sidebar cannot reach error or blocked on the checks read alone. session.pr-sidebar is the family that records it: the adapter drives loadPrSidebarData against the scripted client, so the recorded state is the PrSidebarState the user's screen is built from.
  • Send side. No member is coerced on the way back to the host. The hosted-review provider is only ever compared to 'github' (mobile-pr-sidebar-state.ts:91) and never forwarded, and the repo slug that is forwarded is owner/repo/host strings with no enum anywhere in the path.
  • Pin. baseline is the branch's last commit touching a fenced path (ccd228af24), which is what --record requires. 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

  • Decode, branch vs origin/main after the second merge: 693 header-only (baseline on all 727 shared, adapterSha256 on 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 head 135e5d959d all 736 shared goldens move on baseline alone 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 head a4c4ac44e7 every 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's sent to an interleaved ordinal, gave subscriptions a cleanup checkpoint, and made a salvaging read publish a reply-salvage effect. 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 new agentLaunch capability (one of them, matrix-worktree.create-retry-worktree.create-1, now reports Failed to create workspace where main crashed with a TypeError), four are the subscription cleanup checkpoints, and two are this branch's send-sheet goldens picking up the new reply-salvage effect, which names the one dropped tab. Decode, readers vs the before recording: 688 header-only on baseline alone, 42 body-moved, 0 added, 0 deleted. Decode, each repin alone: 730 header-only on baseline, 0 body-moved — the module split, the unused-import removal and the containment fix move no observation. Decode, round 2 vs c8a0bbf65b: 730 header-only on baseline, 0 body-moved, 6 added, no adapterSha256 anywhere, because a new adapter module re-digests nothing recorded through another one.
  • The containment's golden. session.pr-sidebar mounts phase 1 itself — PrSidebarLoadDeps is five client-taking functions, so the adapter calls loadPrSidebarData with 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. At github.prChecks#1 all eleven partitions record kind: ready with the refusal in checksError, where main took the sidebar to error or blocked. pr-sidebar-checks-failure-state, the mutant that routes !checksOutcome.ok back through failureState, fails pr-sidebar-checks-refused and matrix-session.pr-sidebar-github.prchecks-1.
  • Mutants, applied and reverted by hand, tree verified clean after each (the two enum mutants are the reviewer's, kept as evidence the new cases hold). Three were re-run after the first merge, and the containment and openEnum pair again after the second, and all still fail the same tests: the containment's failureState (5 failures — two sidebar unit cases, pr-sidebar-checks-refused, matrix-session.pr-sidebar-github.prchecks-1, and the registered mutant test), the openEnum swap (3), and the check-summary .catch (1):
    • (a) the created-terminal tab made a salvaged optional again fails matrix-session.pr-triage-session.tabs.createterminal-1 at pr-triage-launch.inner-ok-missing:launched (settlements.launch.error.category expected Error, actual TypeError), and fails the unit case that requires a created terminal to name its handle.
    • (b) the repo-list reader reverted to rpcUncheckedMemberReader fails matrix-session.worktree-connection-repo.list-1 at terminal-worktree-connection-resolved.result-absent:resolved (expected RpcIncompatibleReplyError, actual TypeError) and trips the ratchet: A new operation validates its reply with rpcResultVariant. Nothing may be added to unchecked-rpc-reader-inventory.ts.
    • (c) the three degrading enums swapped to 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.
    • (d) the check row's status made a salvaged optional defaulting to 'queued' fails drops a check row whose status is an arm the icon cannot draw (['ci','drift','lint'] where the row must leave), and nothing else.
    • (f) the check-summary state and the reaction content each given .catch(...) — the swap from drop-the-block to degrade-to-default — fail exactly drops a checks summary whose state is an arm the header cannot colour and drops 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's readCheckSummary/readReaction returned undefined/null for an unknown arm, and nothing else would have caught the change.
    • (e) z.strictObject on 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.
  • The two readers refactor(mobile): send the last session-route raw-port calls as operations (step 6, migration 2) #21083 brought. Step 6's second migration landed created-terminal-tab and terminal-display-mode-set into 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, so z.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.
  • Tri-state flags. autoMergeAllowed and mergeQueueRequired carry three answers: null is GitHub saying auto-merge is not allowed, undefined is the host not carrying the member. The readers coalesced the null away with ?? undefined, which read a well-formed reply differently from readHostedReview/readPRForBranch, which preserved it explicitly, and from the shared types, which declare boolean | null on both. Dropped at all three sites. No consumer separates the two today (pull-request-auto-merge-availability.ts compares with === true and !== false), and no scenario scripts a null on either flag, so the repin moves all 736 goldens on baseline alone and no body — which is why the two new cases, keeps a null autoMergeAllowed on the branch review and drops a non-boolean and keeps a null autoMergeAllowed and mergeQueueRequired on the PR and drops a non-boolean, are the only oracle. Restoring either ?? undefined fails exactly those two.
  • Parity of the deleted parsers. github-pr-parsers.ts, github-pr-comment-parsers.ts and github-pr-value-readers.ts are gone. Their suite now runs the schemas through the same 39 expectations — the nested author.login reviewer shape, the dropped prRepo, the action_required conclusion, 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, commitImageUpload and abortImageUpload have no reply matrix. replyMatrixSites takes every completion in a family's base scenario, and this chain's later params carry the uploadId the start reply named, so driving clipboard.startImageUpload#1 makes main send an append whose params no scripted step matches. The recorder raises Request params mismatch: clipboard.appendImageUploadChunk#1 and records nothing. Both families were written, probed and removed; the three readers are pinned by the schema unit suite and by the existing well-formed clipboard-image-upload-chunked and clipboard-image-attachment-pasted goldens, which do exercise append and commit.

Gates

ORCA_BACKGROUND_LAUNCH=1: root pnpm tc 0; mobile pnpm run typecheck from mobile/ 0; full mobile suite 602 files / 5,824 passed / 6 skipped; check:code-quality:changed 0 across 54 files (all six sub-gates); format:check 0 over 2,662 files under mobile/; oxlint 0 on the changed TypeScript files; check:max-lines-ratchet and check:readme-local-links 0. No file outside mobile/ changed.

…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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 / rpcUncheckedMemberReader give way to rpcResultVariant, 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 in github-pr-rpc.test.ts as the schemas' parity record.
  • Malformed replies change behaviour deliberately: one method-named RpcIncompatibleReplyError instead of null / [] / a coerced default / a V8 TypeError; 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 baseline repin).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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.prChecks read — loadPrSidebarData now returns ready with checks: [] and a checksError instead of routing through failureState, PRChecksSection renders the message, and PrSidebarData carries 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, the githubPrChecksSchema / githubPrAssignableUsersSchema re-export aliases, PRRefreshOutcome on the branch lookup (replaced by a local GitHubPrForBranchOutcome without the fabricated errorType / fetchedAt), and the readQuickCommands indirection.
  • Repinned the corpus to e5f601586b and re-recorded in place (all goldens move on baseline alone).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. The one earlier inline finding (explicit-null collapse on github-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-sidebar adapter drives loadPrSidebarData directly (no React host) over a normal load and a refused-checks load; all eleven github.prChecks#1 matrix partitions record ready with a checksError, and the new pr-sidebar-checks-failure-state mutant re-routes the refusal through failureState, so the family is proven able to fail.
  • Fixed the empty header wording — the section now renders prChecksSummaryLabel(summary, checksError), reading Checks unavailable rather than No checks when the checks read failed; checksError is 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.

Pullfrog  | Fix it ➔ | View workflow run | Using 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The earlier inline finding (explicit-null collapse on github-pr-read-reply-schema.ts) is addressed in d8d8dddea5, 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 null on autoMergeAllowed (branch review and PR) and mergeQueueRequired — the three ?? undefined collapses are gone, so a well-formed reply now reads exactly the way main's value.x === null ? null : ... parsers did.
  • Pinned the tri-state contract with two schema cases: an explicit null survives, false survives, a non-boolean drops to undefined, and each flag's absence stays undefined.
  • Repinned all 736 goldens on baseline alone plus the manifest — no scenario scripts a null on either flag, so no recorded screen moves.

Pullfrog  | View workflow run | Using 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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/main a 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 — sessionTabCreateTerminal now reads sessionCreatedTerminalTabSchema (requires tab.type === 'terminal' and a non-empty tab.id; terminal/title/terminalTheme salvage-optional; loose so status and newer members pass) and terminalDisplayModeSet takes the unread z.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 baseline and re-recorded — all 736 pre-existing goldens move on baseline alone, #21083's 22 arrive, and exactly one golden body-moves vs main's own recording.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H
Jinwoo-H merged commit 4b87675 into main Sep 17, 2026
22 checks passed
@Jinwoo-H
Jinwoo-H deleted the step7-session-readers branch September 17, 2026 01:24
ivanvykopal pushed a commit to ivanvykopal/orca that referenced this pull request Sep 20, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant