mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* chore(rpc-contract): provisional catalog entries for the mobile web bundle methods PROVISIONAL, and the only commit on this branch that must not survive the merge as written. `rpc-params-catalog.generated.ts` is generated from the host method registry, and A5's client operations cannot name `mobileWeb.bundle.manifest` or `mobileWeb.bundle.chunk` until A3 registers them: `defineRpcOperation` constrains `method` to `RpcMethodName`, which is `keyof typeof RPC_PARAMS_BY_METHOD`. These two entries are what the generator emits once A3 lands. After merging A3, run `pnpm run generate:rpc-params-catalog` and keep its output, not this. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): checked client operations for the desktop-served mobile web bundle Two `defineRpcOperation` descriptors over the A1 contract, both `require-result-or-throw` at `on-settle`: there is no partial success in a bundle read, and a salvage policy would produce a half-bundle that fails a hash check far from the cause. Readers are hoisted `looseObject`s that require only what this client reads, so a later optional member stays a Rule 1 addition for released phones; the host's own schemas stay strict. `dataBase64` is bounded by the contract's chunk size, so a host that overshoots is refused at the boundary rather than at reassembly. `readMobileWebBundleErrorCode` maps the host's six codes out of the thrown `code: message` diagnostic and answers null for everything else. Membership comes from the contract's own enum, which is built from its `hostUnionArms` record, so the arms here cannot drift from the host's union. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): fetch and verify a whole mobile web bundle over the paired connection `fetchMobileWebBundle` reads the manifest, pages every asset at the chunk size the host advertised, and verifies each reassembled asset against the manifest's sha256 before returning it. Nothing is cached and nothing is rendered: this is Phase A's proof that the pipe carries a bundle intact. Four asset reads run at once and no more, because the host refuses the fifth concurrent read on one connection with `mobile_web_bundle_read_limited`; paging inside an asset stays sequential, since the next offset is only known to be wanted once a reply says it is not the last. Every chunk reply restates its build, path and offset and the whole asset's length and hash, and all five are checked. A desktop that auto-updates mid-download answers a later chunk from a different build, and nothing else in the reply says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): dev-only troubleshooting row that fetches the mobile web bundle The Phase A proof that the pipe works on a device. Tapping it fetches the whole bundle from the paired desktop and reports the build, asset count, byte count and elapsed time, or the host's error code. `TroubleshootView` gains a `developerRow` slot and the route fills it only when `__DEV__` is true, so a shipped build mounts nothing: no host lookup, no client acquisition, no request. The row reuses the screen's existing button and check-row styles, so it adds no visual vocabulary. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): recording scenarios for the mobile web bundle operations Two families over the real product modules: `mobileWeb.bundle-manifest` drives the manifest descriptor alone, so the loose reader's verdict on one reply is the whole observation, and `mobileWeb.bundle-fetch` drives the paging flow over a two-asset bundle whose entrypoint spans two chunks. The fetch family's state carries the decoded bytes of every asset rather than a count. A reassembly that misplaces a chunk still has the right length, so only the bytes say so. Goldens land with the repin in the next commit: the recorder fences on the pinned tree, and these modules are not in it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording corpus and record the mobile web bundle goldens `--record` refuses on any tree but the pinned one, and the pin predates this branch's product modules, so the corpus is repinned to `bbf8264425` — the last commit here to touch a fenced path — and re-recorded whole, the way `rpc-recording/README.md` prescribes for a product change. The delta is the clean one that repin predicts. All 778 existing goldens move exactly one line, `baseline`, and nothing else: no body moved, no other header key moved, none was deleted. Nine are added, two pilot per family plus the five reply matrices the two families derive. The fetch adapter projects its result rather than returning it whole. The result carries a Map of Uint8Arrays, the observation refuses a non-plain object, and the first recording lost the settlement and filed an unhandled rejection in its place. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the fake host's params through a boxed field read The changed-code casting gate refuses the assertion the fake transport used to type its recorded params. Boxing the value the way `settings-read-operations.ts` does reads the same fields with no assertion, and a non-object params reads as absent instead of throwing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to this branch's last fenced commit The casting fix landed under `mobile/src`, which is a fenced path, so the pin no longer named the tree `--record` runs on. Repinned to `79c3eed6db` and re-recorded. Every golden moves the `baseline` header and nothing else, which is what a repin with no product change is: the edited file is a test, and no recording loads one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): mutation evidence that the fetch projection observes the bytes Writes every chunk at offset 0, so a multi-chunk asset reassembles as its last chunk over a zero-filled buffer. The length still matches the manifest, so only the sha256 check and the decoded bytes in the projection can see it, which is what the fetch family's state exists to show. The mutant is killed. `mutants/` is outside every golden digest, so this moves no recording. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop every worker's chunk reads the moment one asset fails `stopped` was read only between assets, so the other three workers paged their asset to the end after the fetch had already rejected: 121 chunk requests where 4 had been issued at the rejection. Each one holds one of the host's four read slots, so an immediate retry was refused with `mobile_web_bundle_read_limited` that only the abandoned workers caused. An internal AbortController now stands beside the caller's signal and is checked before every chunk request, not just between assets. Also pins the entry abort check, the overrun check with real bytes, the measured byte total, and a schema refusal whose message is prose rather than one of the six codes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the code anchor and both operation descriptors `RPC mobile_web_bundle_unavailable failed` separates the anchored reader from an unanchored one; the prose test that claimed to cover it had its first token at index 0, so the anchor was load-bearing and untested. Also pins that a schema refusal, which the dispatcher raises with zod prose before the bundle handler runs, reads as no code, and that both descriptors stay `require-result-or-throw` / `on-settle`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): dial the host on tap in the dev bundle row, and name it Opening Troubleshoot in a dev build acquired a client at mount, which is what kicks a dial, on a screen that opened no connection before. The probe now acquires only once the row is tapped, and each request owns its AbortController so a re-run, an unmount or StrictMode's second mount abandons the previous fetch and stops its chunk reads instead of holding the host's read slots. The screen carries no host parameter and troubleshoots every paired host, so there is no host it is "on": the row still takes the first paired host but now names it in the result instead of implying it speaks for all of them. The label says whether it is still connecting or already fetching. There is no `__DEV__`-conditional `require` idiom in this repo to trim the row out of a release bundle with, which the route now records. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): refresh the recorder corpus counts 397 scenarios, 787 goldens, 790 tests from the README's own three-file command. The 44 salvage goldens are unchanged; only the total they are quoted against moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin and re-record for the mid-asset stop Baseline moves toc519c2027d, the last commit on this branch to touch a fenced path, and the whole corpus is re-recorded from it. Delta against the pin, by the README's four classes: 786 header-only, 1 body moved, 0 added, 0 deleted. The only key that moved on the 786 is `baseline`; neither `recorderSha256` nor any `adapterSha256` moved, so nothing this branch touched is inside a hashed recorder path. The one body move is the disclosed behaviour change. `matrix-mobileweb.bundle-fetch-app-js.json` is the reply matrix at the app-js binding: where a partition leaves the app-js chunk without a result, the fetch now stops the other workers mid-asset, so the sender list loses the chunk calls they used to make for a bundle nobody would read. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): hold the rendered tree and the captured signal in boxes Assigning to a `let` inside a callback leaves it narrowed to `null`, which the harness was answering with two type assertions. A one-property box is a checked type and the casting gate no longer has anything to report. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to the branch's final fenced commit Removing the two type assertions touched a test file under `mobile/src`, which is inside the fence, so the pin moves tocae8f4a318and the corpus is recorded again from it. Header-only, as a repin with no behaviour change should be: 787 header-only, 0 body moved, 0 added, 0 deleted, and `baseline` is the only key that moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let runRpcOperation send a params-less method A3 declares `mobileWeb.bundle.manifest` with `params: null`, so the generated catalog types its send params as `void` and the two call sites that pass an explicit `null` stopped compiling. `bindDeferredRpcOperation.request` already solved this: `RpcSendArguments` admits `null` exactly where the catalog declares no params, because `params: null` is not the frame that omits the key and narrowing it would rewrite bytes shipped senders already put on the wire. `runRpcOperation` was the one send entry point that never adopted the tuple, having had no params-less caller until now. The compile fence pins all three accepted shapes and that a params-bearing object is still refused. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus after merging main The merge brought A3's host methods and the generated catalog, and the follow-up widened runRpcOperation, so `mobile/src` and `src/shared` both moved. Repins `baseline` to5be50beb41, the last commit to touch a fenced path, and re-records everything. Delta against that commit: 787 header-only, 0 body moved, 0 added, 0 deleted. The only header key that moves is `baseline` — the transport change is type-only, so nothing a screen observes changed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): bound the bundle a manifest can make the client allocate M1: the loose client reader kept every ceiling A1 declared except the one that bounds their product. A manifest could pass `totalBytes` 0 alongside 256 assets of 10 MiB each and the fetch would allocate 2560 MiB against a 32 MiB contract. The reader now sums `assets[].byteLength` against MOBILE_WEB_BUNDLE_MAX_TOTAL_BYTES. A ceiling rather than the host's sum === totalBytes equality, because this client never trusts `totalBytes` for anything and bounds what it will actually allocate instead. L1: a tap dials the host, and nothing bounded that wait. A host whose client never arrives left the row reading `Connecting…` with its button disabled for the life of the screen. A deadline through the diagnostics folder's own `startDiagnosticFetchTimeout` settles it to a failure and drops the acquisition. Ten seconds, because acquiring a client is local work: the connect and request timeouts live below this and only apply once one exists. L2, four survivors now pinned: the eof break against a zero-byte asset end to end, the offset half of the chunk echo check on its own, the anchor that keeps `rpc (mobile_web_bundle_unavailable)` from reading as a code, and both `abandoned` guards against a run the screen moved on from. Also: the stop check moves above the per-asset buffer, which makes the worker loop's copy redundant; drops the unreferenced chunk reply type; and restores the comment pairing in operation-mutations.ts, where the bundle entry had been inserted between the catalog mutation's comment and its entry. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus after the round-2 fixes Repins `baseline` to3252779fa7, the round-2 product commit, and re-records everything. Delta against that commit: 787 header-only, 0 body moved, 0 added, 0 deleted, and `baseline` is the only header key that moves. `recorderSha256` holds even though `mutants/operation-mutations.ts` changed, because the mutant directory is excluded from the recorder digest on purpose — nothing on the recording path reads it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus onto the merge that carries A4 A4 (#21376) added a mobile/src file inside the recorder fence, so the pin has to name a commit that contains it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb