Skip to content

feat(apple): serve the Simulator AX bridge from the XCTest runner - #4182

Draft
thymikee wants to merge 9 commits into
stack/limrun-native-runnerfrom
feat/runner-hosted-ax-bridge
Draft

thymikee wants to merge 9 commits into
stack/limrun-native-runnerfrom
feat/runner-hosted-ax-bridge

Conversation

@thymikee

@thymikee thymikee commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Limrun iOS snapshots now read the same Simulator AX bridge as a local Simulator. Limrun cannot start the guest bridge process: its simctl spawn accepts only defaults|notifyutil|limproxy, and it forwards no socket. The XCTest runner therefore compiles the shared apple/snapshot-bridge reader and serves the snapshotBridge command off its journal and XCTest main thread. The shared snapshot route keeps the generation circuit, the system-surface proof, and the one typed XCTest fallback. A slow read is abandoned, not fatal to the runner. ADR 0004 is amended.

The runner admits one bridge read at a time. Its bridge-read-in-flight refusal is capture-scoped: a read that a cancelled capture left running costs only the next capture its bridge read, never the app generation. The daemon keeps no read queue of its own.

The Limrun runner must be built with Xcode 26. Limrun simulators run iOS 26, and an Xcode 27 simulator build embeds no XCTest frameworks, so it cannot start there. The release workflow selects Xcode 26, the package script refuses a runner without XCTest.framework, and the Limrun runner products check refuses one before upload.

Review follow-ups in this PR: the provider surface proof reads only the runner session it started; the dead limrun-ios-tree producer is removed; contracts/fixtures/ios-snapshot-bridge-refusals.json classifies every bridge refusal code, and a repo guard fails when an Objective-C, Swift or TypeScript emitter writes a code the fixture does not classify; ADR 0004 records that observe-only captures bypass the route. A bridge read whose continuation finds fewer children than the first read counted (a list scrolling between the reads) is now snapshot-tree-changed with capture scope; before, it was snapshot-tree-malformed and moved every later capture of the app to XCTest (found on Bluesky's feed). The reader's errors are a typed enum, and a foreground change during a continuation is foreground-owner-changed.

On every Simulator, the system-surface foreground reads (one appState per registered host) now go out together instead of one after another.

9 commits. Stacked on stack/limrun-native-runner: the 3 layers of muvaf's Limrun work that are not on main yet (#4174, #4194 and #4195 merged the others), rebased onto current main. The Limrun switch layer ("replace iOS AX automation with the shared XCTest runner") also restores the runner exports that the merged #4174 moved out of layer 1 (createLaunchedAppleRunnerProvider, RUNNER_STARTUP_TIMEOUT_MS, mintRunnerToken, runnerListenerEnv, RUNNER_XCTESTRUN_CAPTURE_OPTIONS). Retarget to main once those layers land. Overlaps #4172 (same bridge/route files).

Validation

Head bacf32eeb3, base 24be24328d (on main 035c2b6cae).

  • FALLOW_AUDIT_BASE=24be24328d pnpm check:affected --run --head bacf32eeb3: passed, exactCommit=true, receipt run-A35qVo.
  • Swift unit tests (iOS 26.2 Simulator, unit-test build): 35 passed, including the shared AX recovery fixture with the new children-shrink-during-continuation case.
  • Bluesky (Release build, signed in, real feed), Limrun iOS 26.4 with the Xcode 26.2 runner from this head, and a local iOS 26.2 Simulator:
    • App captures bridge-served: Limrun 44/46 and local 47/48 per full flow; the other captures are the expected typed fallbacks (notification alert over the app, the capture after a forced --actions XCTest read). Warm p50 0.69 s on Limrun, 0.20 s local. Forced XCTest on the same feed: 4.4–5.2 s with a depth-56 cut; the bridge tree has no cut.
    • Node counts equal on Limrun and local per screen (profile, notifications, settings, search, drawer); feed posts carry text and Reply/Repost/Like controls.
    • Scroll stress, 30 scroll-and-capture cycles with no settle, plus fling cycles, on both targets: no circuit-disabled anywhere; one snapshot-tree-changed (local) cost exactly one capture, and the next was bridge-served.
    • Busy/cancel: 5 short --timeout captures and 2 killed CLIs per target; all 14 following captures bridge-served, no runner restart.
    • The in-app browser (Terms of Service) was read by XCTest with the web sign-in warning; after Done the bridge served again.
  • Deep-tree suite (depth 80/120/200), Settings, fill/type/scroll/record on the test app, sheet probe app: all bridge-served on both targets at earlier heads of this round.

Known issues on main, not in this PR (found during the Bluesky run):

  • Limrun sessions end after 60 s without a CLI call: Limrun gets no connection lease TTL, so the daemon default of 60 s applies (lease-registry-scope.ts). LIMRUN_KEEP_ALIVE pings only the instance. A call every 45 s keeps the session.
  • XCTest coordinate actions on some Bluesky composer elements crash the runner (No element snapshot found for Application … → point.x != INFINITY): fill into the composer on Limrun, and once a tap on the compose button locally. The runner restarts and the next capture is bridge-served.

Not reached live: the native abandoned-read path and a live bridge-read-in-flight refusal (a Limrun bridge read takes ~150 ms, shorter than any cancel-to-next-capture gap); unit tests cover both.

@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Nov 4, 2026 6:27am UTC

Request Review

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.91 MB 5.94 MB +27.0 kB
Package (unpacked) 5.91 MB 5.94 MB +27.0 kB
Package (download) 1.80 MB 1.81 MB +7.9 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 23.0 ms 26.6 ms +3.6 ms
CLI --help 42.1 ms 51.9 ms +9.8 ms

@thymikee
thymikee force-pushed the feat/runner-hosted-ax-bridge branch 2 times, most recently from c2841b2 to 5445f27 Compare November 3, 2026 08:18
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

At 5445f27 I found one likely defect in the cancel path and one missing live run. CI is green (17 checks, 0 not passing), and there are no conflicts. Before merge, bridge-read-in-flight needs to be capture-scoped with a cancel-then-capture route test, and a Limrun system-surface run at this head needs to be attached.

I found this by reading the code, not by running it, so please confirm it. If a caller cancels a runner-served describe (a cancelled request, or a wait/batch step that aborts its capture), send() releases the daemon's previousRead turn at once in the finally. The runner does not see the disconnect, so its reader stays in flight until the AX read returns. The next capture of the same generation then gets application_not_responding / bridge-read-in-flight, which maps to kind timeout. bridgeFailureDisposition does not list that code in CAPTURE_SCOPED_REFUSALS, so the route adds the generation to disabledGenerations. On Limrun, one cancelled capture followed by a capture inside that read's lifetime would move every later capture of the app generation to XCTest. Each of those costs 1-1.6 s plus a target round trip and carries the XCTest warning. The refusal only says the reader is busy, so it is not evidence that the generation cannot be served. The local guest path queues the next read instead of retiring the generation. The rule: a refusal that only says another read is still running is capture-scoped on every channel. CAPTURE_SCOPED_REFUSALS enumerates those, and the exhaustive warning switch needs a consequence for the new entry. With that in place, the daemon-side previousRead queue can go, because the runner's admission is the one owner. The ADR sentence that says only an abandoned read can meet this refusal needs correcting. Please add a route test that cancels a describe, sends a second capture that the runner refuses with bridge-read-in-flight, and asserts the third capture is bridge-served. That test should fail without the fix.

On Limrun, systemSurfacePresent is fixed to unknown. So the runner's appState foreground read is the only thing that keeps a capture off an occluded app tree (#2438). The PR body shows bridge-served captures with no warnings, but no run where a registered host (SafariViewService or PassbookUIService) is foreground over the app. The unit tests stub appState, so a wrong answer on the live branch would serve the occluded app tree with no warning. Please run once on Limrun at this head: open an app, present a web sign-in sheet, and take a snapshot. The output should show producer xctest, the surface tree, and no snapshotBridge describe in the diagnostics for that capture. Then dismiss the sheet and show the next capture served by simulator-ax-bridge. Please also attach the command output for the claimed bridge-served and local Simulator runs.

Not blocking, take or leave: the runner budget at runner-snapshot-bridge.ts:152 is computed before send() waits for the previous read's turn, so a read that queues longer than the margin is aborted by the daemon before the runner abandons it (computing it from the deadline left after the turn, or dropping the daemon queue, fixes this); RunnerSnapshotBridgePrimaryApp and RunnerSnapshotBridgePrimaryPid in RunnerSnapshotBridge.m:26 repeat AgentDeviceForegroundElement and the primaryApp lookup in isPrimaryForegroundProcess: (a shared primaryForegroundPid() in SnapshotBridgeRuntime.m would serve both); the comment at runner-runtime.ts:112 says observe-only reads the runner's tree directly, but the provider capture throws UNSUPPORTED_OPERATION for every observe-only input; createAppleRunnerRuntime never calls snapshotRoute.shutdown(), so the transport close and the source's closed state never run; and the foreground reads in system-surface-foreground.ts:69 now go out together for local Simulators too, so a local capture always sends every appState read (fine to keep, but worth naming in the PR body).

What I did not check: whether a timed-out or cancelled read-only runner exchange on the Limrun session triggers runner recovery above runAppleRunnerCommand; whether the AX reader is safe on a background queue while XCTest issues its own AX queries (only the body's live Limrun claim covers that); bridge-source.ts against adapter.ts line by line (I checked the move by structure only); and the check:affected receipt, Swift unit-test results and live runs from the PR body, which I did not re-run. I also could not confirm whether dropping the XCTest keyboard field on the Limrun bridge path differs from local bridge captures. The bridge tree source has no keyboard field, which matches local Simulator behavior.

@thymikee
thymikee force-pushed the stack/limrun-native-runner branch from 1a5ca97 to f165d9c Compare November 3, 2026 12:50
@thymikee
thymikee force-pushed the feat/runner-hosted-ax-bridge branch from 5445f27 to 7e0f29c Compare November 3, 2026 12:50
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Thanks. Both points are fixed at 7e0f29cf02, and the stack is rebased on current main.

1. bridge-read-in-flight is capture-scoped. You were right. bridge-read-in-flight now has its own consequence (bridge-read-busy) in the failure policy, so the route answers it with the XCTest fallback for that capture only and never retires the generation. The daemon-side previousRead queue is gone; the runner's admission is the one owner. That also removes the budget-before-queue issue you named. New route test: cancel a describe, the second capture meets bridge-read-in-flight and falls back, the third is bridge-served. It fails without the fix. The policy test pins bridge-read-abandoned as generation-scoped. The ADR sentence now says a read a cancelled capture left running meets the refusal too, and that the refusal has capture scope.

2. Live system-surface run on Limrun at this head. No repo app presents a web sign-in sheet, so I used a 60-line UIKit probe app with two buttons: SFSafariViewController and ASWebAuthenticationSession. Same script on Limrun (iPhone, iOS 26.4, Xcode 26.2 runner built from this head) and on a local iOS 26.2 Simulator, snapshot -i --json --debug each time:

capture Limrun producer snapshotBridge cmds Local producer top nodes warning
app simulator-ax-bridge 2 (target + describe, cold) simulator-ax-bridge SheetProbe, Sheet Probe Home —
app simulator-ax-bridge 1 simulator-ax-bridge SheetProbe, Sheet Probe Home —
Safari sheet xctest 0 xctest Safari, Toolbar, Close web sign-in sheet; inapplicable (system-surface-presented)
after Close simulator-ax-bridge 1 simulator-ax-bridge SheetProbe, Sheet Probe Home —
auth sheet xctest 0 xctest Safari, Toolbar, Cancel web sign-in sheet; inapplicable (system-surface-presented)
after Cancel simulator-ax-bridge 1 simulator-ax-bridge SheetProbe, Sheet Probe Home —

Each Limrun sheet capture sent 2 appState + 1 snapshot to the runner and no snapshotBridge. Wall times on Limrun: 0.46–0.53 s bridge, 0.74–1.14 s sheet (first capture 12.4 s includes runner start). One earlier run hit a foreground read past its 1 s connect budget while the sheet was presenting; that capture reported system-surface-probe-unavailable and still went to XCTest with the surface tree, so the fallback stays on the safe side.

Also changed: Xcode 27 is not expected on Limrun. An Xcode 27 simulator build embeds no XCTest frameworks, so it cannot start on Limrun's iOS 26 simulators. The release workflow now selects Xcode 26, the package script refuses a runner without XCTest.framework, and the Limrun products check refuses one before upload with a hint. Docs say so.

Non-blocking items: the observe-only comment is corrected; createAppleRunnerRuntime now has shutdown(), which closes the route and source, and Limrun dispose calls it; the parallel foreground reads are named in the PR body. I left the primaryForegroundPid duplication in RunnerSnapshotBridge.m: sharing it means changing the guest runtime, which I would rather do with #4172.

Gate: FALLOW_AUDIT_BASE=f165d9c46c pnpm check:affected --run --head 7e0f29cf02 passed, exactCommit=true, 42 checks. Swift unit tests: 8 passed.

Still not checked: the native abandoned-read path, and whether a timed-out read-only runner exchange on Limrun triggers runner recovery above runAppleRunnerCommand.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

The code looks good at 7e0f29c. The earlier review issues are addressed, and the Limrun system-surface run you posted covers the occluded case: supplied signal and deadline aborts now become request cancellation without triggering ready-session recovery, while unrelated transport failures retain recovery. All 17 checks passed. There are no conflicts.

Not blocking: The cancellation assertion can accept an unrelated error; you can take or leave an assertion against cancelled.signal.reason, or a named typed cancellation reason, in the cancellation test: https://github.com/callstack/agent-device/blob/7e0f29c/packages/platform-apple/src/runner-snapshot-bridge.test.ts#L324.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 3, 2026
@thymikee
thymikee force-pushed the stack/limrun-native-runner branch from f165d9c to 00d77a6 Compare November 3, 2026 15:00
@thymikee
thymikee force-pushed the feat/runner-hosted-ax-bridge branch from 7e0f29c to fd56bac Compare November 3, 2026 15:00
@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

The code looks good at 3a4eb6d. The surface proof is now fenced to one runner session, the dead limrun-ios-tree producer is gone, and the refusal codes are guarded against drift. All 16 checks pass and there are no conflicts. The native sources have not changed since fd56bac, so the earlier Limrun and local sheet runs still cover them.

Not blocking: You can take or leave extending the emitter guard to enumerate deadline-helper and native-build forwarding paths and classify their codes in the fixture, while keeping bridgeFailureDisposition as the runtime policy owner; the current guard recognizes only literal TypeScript snapshotSourceError calls. Guard

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

The code at c56942e looks good: a tree that changes during a bridge read is now refused for that capture only. This new native path still needs a live run. CI reports 16 passing checks and no failures. There are no known conflicts.

No recorded live run reaches the new refusal on either production channel, so device validation remains incomplete despite green CI (SnapshotBridgeRuntime.m:50). Please rebuild c56942e and record snapshot -i --json --debug on both Limrun and a local Simulator during deep-tree churn. Each run must show snapshot-tree-changed, XCTest fallback, and then simulator-ax-bridge on the same generation; include the commands, device, and SHA. Please attach this evidence before merge.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 3, 2026
thymikee and others added 9 commits November 4, 2026 07:18
Split request validation out of the guest bridge server into SnapshotBridgeRequest.m and name the
reader translation units once (SHARED_SNAPSHOT_BRIDGE_READER_FILENAMES). The guest bridge compiles
them with its socket server; the runner fingerprint and its CI cache key now cover them, because
the runner compiles the same reader. A host that serves the reader for other work can turn off the
watchdog that ends the guest process (SNAPSHOT_BRIDGE_WATCHDOG_ENDS_PROCESS).
createSnapshotBridgeSource owns bounds, depth hints, acquisition, and the recovery record; a
transport supplies only how one accepted envelope arrives. The local guest process is the first
transport. acceptSnapshotBridgeEnvelope is the one acceptance rule for a bridge reply.
A provider iOS Simulator, such as Limrun, cannot run the guest bridge process: its simctl spawn is
restricted and it forwards no socket. The runner now compiles the same reader and answers the
snapshotBridge command off its journal and off the XCTest main thread. It abandons a read that
outlives its budget instead of ending itself. The Apple runner runtime routes captures through the
shared snapshot route with a runner channel, so a provider Simulator gets the local tree, recovery,
depth cut, producer, generation circuit, and typed XCTest fallback.

The runner names the app generation itself (target) and refuses another process (stale_target).
The route refreshes a cached target before it trusts an open circuit, so a relaunch re-enables the
bridge; a failed refresh keeps the cached generation. Only the runner's foreground read can prove
that no system surface covers a provider Simulator, so that read starts a runner that is not up
yet, and the host-side reads now go out together. Runner commands answered off the queue carry an
explicit queued: false trait, so the bridge read is never charged as queued work.
Limrun simulators run iOS 26. An Xcode 27 simulator build embeds no XCTest
frameworks and resolves them from the build machine, so it cannot start on a
remote simulator. The release workflow now builds with Xcode 26, the package
script refuses a runner without XCTest.framework, and the Limrun runner
products check refuses such a runner before upload.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…drift

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The bridge reader re-reads a node whose children the first read withheld.
When a scrolling list recycles its cells between the two reads, the
re-read returns fewer children, and the runtime reported every such
reader error as snapshot-tree-malformed. The route then retired the app
generation, so one scroll moved every later capture of the app to XCTest
(seen on Bluesky's feed). The reader errors are now a typed enum, and the
runtime maps each one to its refusal: a changed tree is
snapshot-tree-changed with capture scope, and a foreground change during a
continuation is foreground-owner-changed.

Co-Authored-By: Claude <noreply@anthropic.com>
@thymikee
thymikee force-pushed the feat/runner-hosted-ax-bridge branch from c56942e to bacf32e Compare November 4, 2026 06:26
@thymikee
thymikee force-pushed the stack/limrun-native-runner branch from 00d77a6 to 24be243 Compare November 4, 2026 06:26
@thymikee

thymikee commented Nov 4, 2026 •

Copy link
Copy Markdown
Member Author

Live evidence for the snapshot-tree-changed refusal at head bacf32eeb3. This is the stack as rebased onto main and force-pushed at 06:26Z; the PR's 9 commits are patch-equivalent to c56942ed24 per git range-diff. On bacf32eeb3, FALLOW_AUDIT_BASE=24be24328d pnpm check:affected --run --head bacf32eeb3 passed with exactCommit=true (receipt run-CokQ9M); I ran it before this live run. Local Simulator: done. Limrun: blocked on the toolchain (details below). The PR stays in draft.

Local Simulator

  • Device: iPhone Air 1B89356B-961C-4993-B0BE-4C0F3917D995, iOS 27.0. Runner built from bacf32eeb3 with Xcode 27.1 into a private derived path. Channel: guest-process bridge.
  • App: Agent Device Tester release build (embedded JS, includes Heavy feed), com.callstack.agentdevicelab.
  • Churn: Heavy feed (30 wrappers; 13–21 continuations per read at 32 accepted levels). A swipe fling followed at once by a capture, so momentum scrolling recycles FlatList cells between a node's first read and its continuation re-read. No product code was changed.

Commands (worktree CLI at bacf32eeb3, private state dir):

AD="env AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH=/private/tmp/ad-4182-local2/derived node bin/agent-device.mjs --state-dir /private/tmp/ad-4182-local2/state --session ax4182b --platform ios --udid 1B89356B-961C-4993-B0BE-4C0F3917D995"
$AD open com.callstack.agentdevicelab --relaunch
$AD open com.callstack.agentdevicelab 'agent-device-test-app://heavy-feed'
# repeated until the refusal appeared (iteration 2):
$AD swipe 210 300 210 880
$AD snapshot -i --json --debug
# then, after the scroll came to rest:
$AD snapshot -i --json --debug

--debug stderr carries only the daemon request lines. The route lines below are from the request diagnostics logs that --debug writes (state/sessions/ax4182b/requests/<requestId>.ndjson). They are raw except that unrelated retry/exec_command/appState lines are left out.

1–2. Refusal, then XCTest serves that capture (request 34837628e06f38d1). After a swipe, the snapshot command takes stabilization captures. The first two are bridge-served. The third meets snapshot-tree-changed, and XCTest serves it, and that XCTest tree is the one returned. Its first warning is: Simulator AX snapshot unavailable (snapshot-tree-changed); used XCTest for this capture, while the screen changed during the bridge read.

{"ts":"2026-11-04T07:04:41.753Z","level":"debug","phase":"ios_snapshot_source_recovery","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","data":{"producer":"simulator-ax-bridge","targetId":"1B89356B-961C-4993-B0BE-4C0F3917D995:com.callstack.agentdevicelab","generation":"15938:UIKitApplication:com.callstack.agentdevicelab[268e][rb-legacy]:Wed Nov  4 08:01:44 2026","requestedLevels":129,"hint":"hinted","hintedLevels":32,"requests":22,"rejected":0,"continuations":21,"acceptedLevels":32,"truncated":false,"learning":"kept"}}
{"ts":"2026-11-04T07:04:41.753Z","level":"info","phase":"ios.snapshot-source.acquire","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":333,"data":{"producer":"simulator-ax-bridge","transport":"guest-process"}}
{"ts":"2026-11-04T07:04:41.756Z","level":"info","phase":"ios.snapshot-source.present","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":3,"data":{"producer":"simulator-ax-bridge"}}
{"ts":"2026-11-04T07:04:42.296Z","level":"debug","phase":"ios_snapshot_source_recovery","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","data":{"producer":"simulator-ax-bridge","targetId":"1B89356B-961C-4993-B0BE-4C0F3917D995:com.callstack.agentdevicelab","generation":"15938:UIKitApplication:com.callstack.agentdevicelab[268e][rb-legacy]:Wed Nov  4 08:01:44 2026","requestedLevels":129,"hint":"hinted","hintedLevels":32,"requests":22,"rejected":0,"continuations":21,"acceptedLevels":32,"truncated":false,"learning":"kept"}}
{"ts":"2026-11-04T07:04:42.296Z","level":"info","phase":"ios.snapshot-source.acquire","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":321,"data":{"producer":"simulator-ax-bridge","transport":"guest-process"}}
{"ts":"2026-11-04T07:04:42.298Z","level":"info","phase":"ios.snapshot-source.present","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":2,"data":{"producer":"simulator-ax-bridge"}}
{"ts":"2026-11-04T07:04:42.824Z","level":"error","phase":"ios.snapshot-source.acquire","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":308,"data":{"producer":"simulator-ax-bridge","transport":"guest-process","error":"iOS Simulator snapshot source malformed-tree: snapshot-tree-changed"}}
{"ts":"2026-11-04T07:04:42.824Z","level":"debug","phase":"ios_snapshot_route_fallback","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","data":{"reason":"snapshot-tree-changed","deviceId":"1B89356B-961C-4993-B0BE-4C0F3917D995","generation":"15938:UIKitApplication:com.callstack.agentdevicelab[268e][rb-legacy]:Wed Nov  4 08:01:44 2026","details":{"guestMessage":"snapshot continuation children unavailable","bridgeFailure":"malformed-tree","bridgeFailureCode":"snapshot-tree-changed"}}}
{"ts":"2026-11-04T07:04:47.971Z","level":"info","phase":"ios_runner_command_send","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":5143,"data":{"command":"snapshot","commandId":"runner-c54196e3-061d-4ec3-893c-f6957caddb34","readOnly":true,"sessionReady":true,"timeoutMs":44999}}
{"ts":"2026-11-04T07:04:47.971Z","level":"info","phase":"snapshot_capture","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","durationMs":5146,"data":{"backend":"xctest"}}
{"ts":"2026-11-04T07:04:47.974Z","level":"warn","phase":"post_gesture_snapshot_stabilization_timeout","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot","data":{"action":"swipe","attempts":3,"durationMs":6569,"lastPairAgreed":false}}
{"ts":"2026-11-04T07:04:47.975Z","level":"info","phase":"request_success","session":"ax4182b","requestId":"34837628e06f38d1","command":"snapshot"}

3. Next capture: simulator-ax-bridge on the same generation (request 30adede85db36841; generation 15938:…[268e]…08:01:44 in both):

{"ts":"2026-11-04T07:04:51.506Z","level":"debug","phase":"ios_snapshot_source_recovery","session":"ax4182b","requestId":"30adede85db36841","command":"snapshot","data":{"producer":"simulator-ax-bridge","targetId":"1B89356B-961C-4993-B0BE-4C0F3917D995:com.callstack.agentdevicelab","generation":"15938:UIKitApplication:com.callstack.agentdevicelab[268e][rb-legacy]:Wed Nov  4 08:01:44 2026","requestedLevels":129,"hint":"hinted","hintedLevels":32,"requests":22,"rejected":0,"continuations":21,"acceptedLevels":32,"truncated":false,"learning":"kept"}}
{"ts":"2026-11-04T07:04:51.506Z","level":"info","phase":"ios.snapshot-source.acquire","session":"ax4182b","requestId":"30adede85db36841","command":"snapshot","durationMs":316,"data":{"producer":"simulator-ax-bridge","transport":"guest-process"}}
{"ts":"2026-11-04T07:04:51.510Z","level":"info","phase":"ios.snapshot-source.present","session":"ax4182b","requestId":"30adede85db36841","command":"snapshot","durationMs":3,"data":{"producer":"simulator-ax-bridge"}}
{"ts":"2026-11-04T07:04:51.511Z","level":"info","phase":"request_success","session":"ax4182b","requestId":"30adede85db36841","command":"snapshot"}

The refusal had capture scope: the generation was not retired (learning: kept, no disabled), and the next capture of the same app process used the bridge again. The same three steps showed up at c56942ed24 before the rebase (requests 9e1372624101587e → bfa156ecb40e8473, generation 44988:…07:31:08). Sessions closed with --shutdown; the daemons are stopped and the iPhone Air is shut down.

Limrun: blocked

This host has only Xcode 27.1 and iOS 27 runtimes; no Xcode 26 is installed. So I cannot build the release-shaped Limrun runner from this head, and the release runner (v0.21.36) does not contain this PR's native change.

As a fallback (at c56942ed24, before the rebase), I built the runner with Xcode 27.1 and embedded the Xcode 27.1 simulator XCTest frameworks, the same set that the v0.21.36 runner carries. I pointed AGENT_DEVICE_LIMRUN_IOS_RUNNER_APP at it:

AGENT_DEVICE_SESSION=ax4182-limrun AGENT_DEVICE_LIMRUN_IOS_RUNNER_APP=<that .app> \
  node bin/agent-device.mjs --state-dir /private/tmp/ad-4182-limrun/state connect limrun --platform ios
… install com.callstack.agentdevicelab ./AgentDeviceTester.app   # Installed
… open com.callstack.agentdevicelab                              # Opened
… snapshot -i --json --debug

The runner never accepted a connection on the Limrun iOS 26 simulator:

{"level":"info","phase":"limrun.runner.launch","data":{"path":"/xctest/run","port":36657}}
{"level":"error","phase":"ios_runner_command_send","data":{"command":"uptime","sessionReady":false,"error":"Runner did not accept connection"}}
{"level":"error","phase":"request_failed","command":"alert","data":{"error":"Runner did not accept connection (launcher exited early)"}}

This matches the Xcode 27 limit that the PR documents. That runner shape is not the shipped one, so this run says nothing about the PR code on Limrun. The Limrun instance I started (lease 8a7c0b9b3d0a8d8922e2b083e07df4f5, ios_euna_01m6jm9gvbfaessq9ttdh4sy3q) is terminated per the Limrun API.

To finish the Limrun run, one of these is needed: an Xcode 26.x install on this host (xcodes install 26.4 needs the owner's Apple ID), or a Xcode 26 runner build of bacf32eeb3 from CI. release-ios-runner.yml uploads to a release tag, so I did not dispatch it. With either one, I can rerun the same heavy-feed churn on Limrun.

🤖 Generated with Claude Code

@thymikee thymikee closed this Nov 4, 2026
@thymikee thymikee reopened this Nov 4, 2026
@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

At bacf32e, local live evidence now covers guest-process transport, but the Limrun run could not start the Xcode 27-built runner on its iOS 26 simulator. Could you run this head with an Xcode 26 runner on Limrun and capture snapshot-tree-changed, the XCTest fallback, and a subsequent bridge-served capture on the same generation? The runner bridge refusal remains unverified on the principal changed device route.

CI reports 18 passing checks and no non-passing checks at head. No conflicts are known. Please post the head-specific Limrun evidence before merge.

This branch has not been deployed

No deployments
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