The device harnesses polled `activation.json` to learn which build was live.
A host now keeps exactly one committed generation, so that directory is the
record. The crash-loop and corrupt-cache drills only ever asserted the A/B
rollback and are removed with it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The Source Control journey looked for a `sourceControl.reviewOpen` bridge
error, which the generic host lane no longer reports under that name. The
observation now records the method a host request named, and the journey
matches on it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The store now takes one complete asset per call and one commit that verifies
the staged tree against the manifest, renames it into place, and keeps a single
generation per host under a four-host LRU cap. That retires stage ids, per-chunk
hashes, byte reservation, the A/B activation record, and the exact-JSON scanner:
the build id is the sha256 of the manifest bytes, so the directory name
authenticates the manifest and the manifest authenticates every asset.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Under --adversarial-content the journey had no native baseline, so it derived
the expected changed-file label from the page it was checking and then asserted
the page contained it. That assertion could never fail. The expected label now
comes from the adversarial repository fixture that created the changed file, and
the journey refuses to run when neither an independent path nor a native
baseline is available.
The workspace-row tap that follows the journey moved next to it, so the step
owns the row it returns to and the runner stays under the .mjs line ceiling.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Session snapshot/feed/activate/close/createBrowser/createTerminal,
quick commands and agent options now run as Desktop mobileWeb.* adapters
behind the generic host-request forwarder instead of shell translators.
Voice, notification, terminal, browser, chat, About and diagnostics
settings render as hosted page routes with native-*/web-* operation
backends. Adds the hosted WebView e2e journeys used to validate them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Record the verified Terminal consumer journey alongside Chat and Browser results. The optional OTA crash-loop fixture is deferred outside the worktree under the requested YAGNI scope.
Exercise text scale, autocorrect and custom shortcut persistence through the real hosted settings and session. Reuse picker and terminal inspection logic and restore original preferences. Full iOS adversarial run passed with ok:true against build eab7fd8f5eb4a37e593526e90dcc5105552691db072da8a982cd1ed30784a2e3; all code gates and export passed.
Share native presentation and apply paired-host page preferences to terminal link behavior. Preserve inherited native values, handle storage failures, and verify saved values plus Chat route recovery through a real iOS WebView restart.
Reuse the Chat UI settings screen with paired-host page storage and grant-gated navigation. Verify persistence and theme rendering in the full iOS adversarial journey; retain native recovery presentation.
Wait for the diff body rather than just Review controls, and handle Back through Source Control before opening file previews. iOS confirms terminal links and rendered adversarial diff; the full unattended journey remains tracked. Required gates pass in native-chat-read-gates.
Keep authority on failed or malformed tab snapshots. Exercise headless terminal file links through line-addressed hosted previews, since renderer-backed file tabs require a Desktop renderer. Record the observed native tap and host resolution evidence.
Label standalone toolbar actions, match WebView accessibility values, and fence workspace return checks by pathname. Reuse Android labelled navigation and support adversarial journeys without native screenshot baselines. Record passing iOS file parity and the unresolved terminal file-link gate on both platforms.
The hybrid app pulled its 9.1 MiB mobile-web package one 48 KiB chunk per RPC
round trip, strictly serially, and 99.3% of the package is a single 9.5 MB
script — so the whole download was 245 sequential round trips against whatever
the relay path's latency happened to be. Measured on the real package over the
real mobile relay client with a 120 ms round trip: 31.7 s (0.30 MB/s).
Two changes, both negotiated:
- the downloader now keeps up to MOBILE_WEB_PACKAGE_MAX_CONCURRENT_READS reads
in flight across the whole manifest while still draining to the stager in
offset order (the native stage appends at the file's current length), and
narrows the window instead of failing when a host answers
mobile_web_package_read_limited;
- mobileWeb.package.asset.gzip takes an optional `length` so one read answers up
to eight chunks. The params schema is strict, so older hosts reject the field —
it is gated on the new mobileWeb.package.range.v1 capability, and the client
still splits the range into 48 KiB stage writes.
Same package, same harness, relay path: 31.7 s -> 2.8 s at 120 ms RTT and
62.9 s -> 5.5 s at 250 ms RTT, with 245 requests down to 76.
The download still dies when the app is backgrounded past
RELAY_BACKGROUND_GRACE_MS — the session suspends, the refresh effect aborts, and
the stage is discarded — so the progress panel now says to keep the app open.
Resuming from the staged offset needs a native stage-reopen API on both
platforms and is not attempted here.
mobile/scripts/measure-mobile-web-package-download.mjs drives the real
downloader over both the direct and relay mobile clients with an injectable
round trip, which is where every number above comes from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The failure printed only landmarkDelta, which cannot say which side moved. The
two landmarks come from different sources, a native accessibility frame and a
hosted DOM rect, so the absolute pair is what makes the next failure readable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The landmark delta is bit-identical across the run before the change and the
run after it (0.047498512585812364 both times), so re-reading after the settle
changes nothing and the animation rationale was wrong. Reverting rather than
leaving an inert change carrying a disproved explanation.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Opening a changed file from a session-origin Source Control sends
files.openDiff, which needs a renderer notifier. The e2e pairs against
`orca serve --mobile-pairing`, which has none, so the host answers
renderer_unavailable on native and hybrid alike. The journey asserted the diff
tab unconditionally and could never pass.
The native baseline now probes the same session-origin path and records what
the paired host answered. The hosted journey takes whichever arrives first, the
diff route or the bridge error, and pins it against the native result, so the
claim is "hybrid matches native on this host" rather than "hybrid fails". On a
full desktop the diff route wins and reviewOpen.headless records false.
Kept as evidence, not fixed: the hybrid banner reads "Source control action
failed" where native names renderer_unavailable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
A single accessibility read caught the Tasks list mid-load on one client and a
second, transient xterm helper node on the other, and both read as client
differences. Poll until two consecutive volatile-free reads match, then
screenshot. Re-running the A/B with this in place left the twelve stops
identical except for live GitHub issue content.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The host-origin Source Control route animates in, so captureNativeRoute read
its landmark before the 500ms settle and screenshotted after. The parity check
then failed on landmarkDelta.y 0.0475 against a 0.005 budget while the pixels
agreed at 0.0121 against 0.03. Measured on a live hybrid session: the DOM point
and the accessibility point agree to 0.0000064, so the coordinate spaces were
never the problem.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The Agent History journeys still asserted "Failed to resume session." for a
page-synthesized resume. mr-gesture-delete removed that gate on purpose, so the
synthetic click now resumes and the wait timed out on the resumed session route.
Both journeys keep the native-touch resume.
Adds a native journey capture that walks the six baseline journeys recording
ordered accessibility labels plus a screenshot per stop, a diff over two
captures, and an ORCA_E2E_MOBILE_METRO_DIR override so an old client checkout
can serve Metro while the paired desktop runtime stays on this one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
One `orca emulator gesture` call delivers its whole point list as a single
batch, so the 62-point begin/hold/end sequence landed as a tap: the native
Source Control baseline long-pressed the workspace row, navigated into the
session instead, and failed on a missing "Source Control" control. Split the
press across two commands, hold, poll for the settled control while the touch
is down, and always lift.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The hybrid WebView required a recent native-observed touch before a bridge
capability could run. A scroll armed the window, so it gated nothing an
attacker on the first-party page could not already reach, and no peer hybrid
framework (Capacitor, Cordova, RN WebView) gates bridge calls this way.
Removes the gesture module, its React authority hook, the shared requirement
and its census, and all 20 gate sites: native.alert, clipboard write, external
link open, terminal text-scale and custom-key updates, dictation start and
model management, account select and reset-credit consume, agent-history
resume, terminal clipboard paste and image attach, navigation reconnect,
removeHost and terminal-settings route, and both workspace-creation writes.
Each operation now just runs.
The AppState foreground reporting the gesture hook also carried moves to
use-mobile-web-app-foreground-authority. permission_required stays in the
bridge error enum: it parses inbound responses, so narrowing it would break a
new page paired with an older shell.
Drops the G3 gesture-window e2e probe and renames its runner to
run-hosted-ios-webview-app-bound-probe.mjs, which keeps the app-bound probe.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
No WKAppBoundDomains is declared, and an A/B native rebuild showed identical
behavior with and without the flag: the shell's navigation delegate refuses
external navigation either way. The probe script stays as the regression check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
#10179 ("bound OOM-prone accumulators") was reverted on main by #10255, but
the hybrid WebView rewrite carried parts of it forward wherever the revert
could not apply cleanly. Remove those, keeping the hybrid work that had
merely adopted them.
Deleted the bounded mobile RPC queues, ledgers, budgets and JSON admission;
the bounded emulator-script readers; the transcript record buffer and
retention window; and the bounded relay/SSH directory and markdown-document
listing modules. Restored main's connection log store, E2EE v2 channel,
daemon spawner and relay error codes, and reverted the transcript readers'
page and drain budgets.
Kept, and rewired onto main's modules: the terminal binary frame path, the
hosted privacy redactions, the transcript file-source plumbing, and the
chunked file and terminal-artifact reads the hosted client depends on.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Two security-review questions about the hybrid iOS shell needed measurement
rather than reading: which native touches arm the 5s user-gesture window that
privileged page requests spend, and whether limitsNavigationsToAppBoundDomains
does anything while app.json declares no WKAppBoundDomains key.
The gesture probe drives clipboardWrite, the cheapest gesture-gated mutation,
through the page bridge after each candidate arming action and records whether
the shell granted or denied it. The app-bound probe asks the page to navigate
to an external https origin and records who refuses it: the shell's navigation
delegate raises a native warning banner, while an app-bound refusal would fail
the provisional navigation inside WebKit with no delegate decision.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Hosted iOS security e2e (three branch-only harness bugs):
- `simctl privacy grant` terminates the app; the relaunch cold-resumes
straight onto the session route, so skip workspace-row activation when
the hybrid handoff already landed on /session/. Activation failures now
name the document href.
- Uploaded-path regex fused two abutting orca-paste-*.png paths; use a
lazy segment quantifier.
- Photos orders by capture date, which addmedia takes from file birth
time; stage a freshly written fixture copy (copyFile clones birth time
on APFS) so "last Photo" is the fixture.
Root tests:
- Allowlist walker skips -test-fakes / -test-fixture(s) / .test-support
sources, which are not mobile call sites.
- Drop the height-only OSC-8 test: main's #17759 clears restored ranges
on any dimension change and pins it.
- Drop serve-update-handoff-startup-order.test.ts: main pins the same
order against main-process-preflight.ts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
native.alert is now gesture-gated, so the simulator journey taps the page
before posting the probe. The task project request client sat one line over
its max-lines cap; its project echo check moves to its own module.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.
Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.
2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.
Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:
- Modules inside `src/shared` import the barrel as `./types`, not
`shared/types`. A pre-filter on the latter string skipped 176 of them and
left imports dangling at a deleted file, which surfaced as confusing
`Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
errors rather than "module not found".
- The barrel RENAMED one type on the way through
(`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
TypeScript parses that `;` as the import statement's terminator, so
replacing through `statement.getEnd()` deletes it and breaks ASI. The
rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
from it, because the barrel re-exported those same names — which trips
`import/no-duplicates` under `--deny-warnings`. A post-pass merges
declarations sharing a specifier and type-only-ness; the `import type` plus
`import` pair from one module is left alone, since that form is allowed.
Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.