Skip to content

[agent-cli] runtime:reload and smoke send no broadcast while the run skips an app - #207

Merged
vonovak merged 27 commits into
codex/reload-context-prooffrom
vonovak/fix-reload-skipped-apps
Oct 30, 2026
Merged

vonovak merged 27 commits into
codex/reload-context-prooffrom
vonovak/fix-reload-skipped-apps

Conversation

@vonovak

@vonovak vonovak commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.

This PR makes a reload reach only this worktree's apps when the run skips an app, and prove the reload with this worktree's apps only.

Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 7/8).

Problem

The dev server sends its reload broadcast to every app on its port. N2-17: a worktree bound to an iOS EAS session ran runtime:reload. It printed "Skipped 1 app on local simulators … iPhone 17 Pro", then "Reloaded yes · via the dev server's client command socket … 2 client(s)". The broadcast also reloaded the skipped app.

Fix

Rung 1 is decided on one target list read after the bundle gate. While the scope skips an app, rung 1 sends no broadcast. It sends CDP Page.reload to each own target and proves each with the context marker, under one shared deadline. When nothing is skipped, rung 1 stays the broadcast, and the attempt says when it also reached an app outside the scope. Peer churn beside an app outside the scope makes the attempt ok: false, delivered: true: the reload then needs a fresh own target or a bundle. Nothing of an app outside the scope proves the reload. The failure text says whether nothing was watched, the churn was discounted, or the watched observations did not happen. When no own app could keep the marker, it says no Page.reload was sent. It names --eas and the tunnel only on a cloud run. Page.reload has 8 s to prove itself, and the first proof stops the other checks (stopped). A Page.reload whose answer was lost is not delivered. When the read before rung 1 fails, the run keeps its first placement and sends nothing (mechanism none). After a relaunch that nothing can prove, local or cloud, the run exits 22 at once instead of waiting out --timeout. CI runs the reload cases that need a local simulator on the macOS job.

Red tests: rungOne-test "a broadcast beside an app nothing places: no bundle proves it"; e2e runtime-target-selection-test "sends Page.reload to its own app only, and a page of the skipped app proves nothing ($pageReload)", "sends no Page.reload when its own app cannot keep the marker" and "runtime:reload does not take another platform's churn as its proof".

Live check (before the 8 s window and the relaunch exit; not rerun since): at the stack top, on an SDK 58 development build on a local iOS simulator. With a second app on another simulator, the run skipped it as other-device, sent Page.reload to its own app only, and proved 4 of 4 reloads by debugger-context-replaced. With one app, it broadcast and proved 3 of 3 by message-socket-peers. On an EAS iOS session (Expo Go, v2 tunnel) with a local simulator app on the same dev server: runtime:reload --eas skipped the local app as other-device and proved 2 of 2 reloads by debugger-context-replaced; runtime:reload --ios skipped the cloud app and proved 3 of 3 the same way.

Watch: when rung 1 broadcasts, an app that attaches after the last target list also gets the broadcast.

🤖 Generated with Claude Code

@vonovak
vonovak marked this pull request as ready for review October 24, 2026 03:41
@vonovak
vonovak requested a review from Kudo October 24, 2026 03:41
@vonovak
vonovak marked this pull request as draft October 24, 2026 05:08
vonovak added a commit that referenced this pull request Oct 24, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server)

Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no
platform flag waited 120 s and exited 22. The run decided "an app is
attached" from the platform flag, before the device was known. With no
flag it counted the debugger targets of every platform, so a local
simulator's app on the same Metro counted as attached, and the launcher
open for the session did not happen. A no-flag navigate to a local
Android emulator with only an iOS app attached made the same mistake.
`navigate --ios` that fell back to an EAS Simulator iOS session without
`--eas` had the reverse fault: the flag scope cannot place the session's
app, so the run reopened an attached app through the launcher.

Change: `openRouteAsync` builds one attach waiter from the scope of the
device it resolved, and asks it whether an app is attached before it
decides on the launcher open. The same waiter serves the launcher wait
and the route-link wait, and reads the device facts once, at its first
probe. The route resolver no longer reports an attached flag. llp/0005
states the rule in the section on pointing an app at this dev server.

This commit sits on the batch-2 stack (#202-#207, RunScope in
src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps.

Red before: navigateAsync on a development build that is not loaded >
opens the launcher URL first, when no platform is named and only an iOS
app is attached; navigateAsync on an EAS Simulator session > opens, with
no flag, an Android session, only an iOS app attached; navigateAsync on
an EAS Simulator session > opens, with --ios without --eas, an iOS
session, its app attached

Guard: navigateAsync on an EAS Simulator session > opens, with no flag,
an Android session, its app attached; navigateAsync on an EAS Simulator
session > opens, with no flag, an iOS session, its app attached

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vonovak
vonovak added this pull request to stack #210 October 26, 2026 09:54
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from d1b95e1 to 9b2210d Compare October 26, 2026 17:44
vonovak added a commit that referenced this pull request Oct 26, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server)

Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no
platform flag waited 120 s and exited 22. The run decided "an app is
attached" from the platform flag, before the device was known. With no
flag it counted the debugger targets of every platform, so a local
simulator's app on the same Metro counted as attached, and the launcher
open for the session did not happen. A no-flag navigate to a local
Android emulator with only an iOS app attached made the same mistake.
`navigate --ios` that fell back to an EAS Simulator iOS session without
`--eas` had the reverse fault: the flag scope cannot place the session's
app, so the run reopened an attached app through the launcher.

Change: `openRouteAsync` builds one attach waiter from the scope of the
device it resolved, and asks it whether an app is attached before it
decides on the launcher open. The same waiter serves the launcher wait
and the route-link wait, and reads the device facts once, at its first
probe. The route resolver no longer reports an attached flag. llp/0005
states the rule in the section on pointing an app at this dev server.

This commit sits on the batch-2 stack (#202-#207, RunScope in
src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps.

Red before: navigateAsync on a development build that is not loaded >
opens the launcher URL first, when no platform is named and only an iOS
app is attached; navigateAsync on an EAS Simulator session > opens, with
no flag, an Android session, only an iOS app attached; navigateAsync on
an EAS Simulator session > opens, with --ios without --eas, an iOS
session, its app attached

Guard: navigateAsync on an EAS Simulator session > opens, with no flag,
an Android session, its app attached; navigateAsync on an EAS Simulator
session > opens, with no flag, an iOS session, its app attached

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from 9b2210d to c9ea36d Compare October 26, 2026 18:24
vonovak added a commit that referenced this pull request Oct 26, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server)

Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no
platform flag waited 120 s and exited 22. The run decided "an app is
attached" from the platform flag, before the device was known. With no
flag it counted the debugger targets of every platform, so a local
simulator's app on the same Metro counted as attached, and the launcher
open for the session did not happen. A no-flag navigate to a local
Android emulator with only an iOS app attached made the same mistake.
`navigate --ios` that fell back to an EAS Simulator iOS session without
`--eas` had the reverse fault: the flag scope cannot place the session's
app, so the run reopened an attached app through the launcher.

Change: `openRouteAsync` builds one attach waiter from the scope of the
device it resolved, and asks it whether an app is attached before it
decides on the launcher open. The same waiter serves the launcher wait
and the route-link wait, and reads the device facts once, at its first
probe. The route resolver no longer reports an attached flag. llp/0005
states the rule in the section on pointing an app at this dev server.

This commit sits on the batch-2 stack (#202-#207, RunScope in
src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps.

Red before: navigateAsync on a development build that is not loaded >
opens the launcher URL first, when no platform is named and only an iOS
app is attached; navigateAsync on an EAS Simulator session > opens, with
no flag, an Android session, only an iOS app attached; navigateAsync on
an EAS Simulator session > opens, with --ios without --eas, an iOS
session, its app attached

Guard: navigateAsync on an EAS Simulator session > opens, with no flag,
an Android session, its app attached; navigateAsync on an EAS Simulator
session > opens, with no flag, an iOS session, its app attached

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vonovak
vonovak removed this pull request from stack #210 October 27, 2026 17:52
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from c9ea36d to 4e65852 Compare October 27, 2026 17:53
@vonovak
vonovak added this pull request to stack #212 October 27, 2026 17:53
@vonovak vonovak changed the title [agent-cli] runtime:reload and smoke reload only this worktree's app while the scope skips one (llp/0005 §Target owner) [agent-cli] Runtime reload and smoke reload only their own targets while the scope skips an app Oct 27, 2026
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from 4e65852 to 746c2f0 Compare October 27, 2026 19:38
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from 746c2f0 to e4fa28a Compare October 27, 2026 21:29
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from e4fa28a to 695047e Compare October 27, 2026 21:52
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from 695047e to b8807cd Compare October 27, 2026 22:01
vonovak added a commit that referenced this pull request Oct 29, 2026
…207 changelog entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vonovak
vonovak force-pushed the vonovak/fix-reload-skipped-apps branch from be2a099 to fc40664 Compare October 29, 2026 19:52
vonovak and others added 27 commits October 30, 2026 10:34
Rung 1 is decided on one observation made after the bundle gate: a fresh
target list and the device facts that place it, read inside the run's
remaining budget. Its scope, inventory and classified list serve the rest
of the run: dispatch, evidence, counts and the relaunch. smoke reads the
same budgeted observation for its reload.

While the scope skips an app, rung 1 sends no broadcast. It sends CDP
Page.reload to each own target that takes it, proves each on its target
with the context marker, and keeps each target's outcome. All targets
share one deadline. One replaced context proves the reload.

When nothing is skipped, rung 1 stays the broadcast, also beside an app
outside the scope, and the attempt says that it also reached that app.
A fresh target, appsConnected and the relaunch app id count only the
read group of the whole list. Peer churn beside an app outside the scope
is "acted", not proved: the attempt is delivered, not ok, and the reload
then needs a fresh own target or a bundle. A bundle proves only for a platform that no app outside the read
group can fetch: never one a skipped app is on, and none after a
broadcast that reached an app nothing places.

runtime:reload and smoke end the reload in one function: it skips the
wait when a new context already proved the reload, or when a Page.reload
left no bundle that may count, and it names the strongest label whose
evidence is in the payload. The failure text says which state the run
ended in: nothing was watched, the churn was discounted, or the watched
observations did not happen. Its How line names --eas and the tunnel
only on a cloud run, and says no Page.reload was sent when no own app
could keep the marker.

The rules are in llp/0005 §How it reloads, §What proves a reload and
§Target owner. The label rule and the evidence wait move to
src/runtime/reload/evidence.ts, and the reloadAsync.ts cap goes down to
860.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reload cases beside another worktree's app become e2e cases in runtime-target-selection-test:
Page.reload goes to this worktree's app only, a kept context is not counted, an app with no
debugger gets no Page.reload, an app that appears during the bundle gate gets no broadcast, the
device fallback stops only this worktree's app, and another platform's churn is not counted. The
stub dev server gains a `pageReload` option and records Page.reload on a socket with no debugger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The other-worktree row had no device of its own: it moved the only binding to the other worktree,
so the own app ran on a device nothing placed. It now binds its own simulator and puts another
worktree's clone beside it, with the shared fixtures (bindOtherWorktreeAsync, runtimeTarget) instead
of a hand-built registry file and a unit-test fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reload rules beside a skipped app cite the runtime-target-selection and smoke-target-selection
e2e cases that replaced their unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The smoke reload rows and the bundle-gate re-read case read a booted local simulator, which exists
only on macOS; on Linux and Windows they failed without testing the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the scope JSON and the skipped-app reason

The bound simulator fixture carries a udid, an unused fixture import goes, and phases.ts holds the skipped-app reason together with this layer's lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hing can prove (llp/0005 §Target owner)

A Page.reload and a local relaunch bring the app back under the page id
it had. When the mechanism does that, the own app was listed before the
run, and no bundle may count, the evidence wait is skipped. The rule
read only whether the last attempt was a Page.reload, so a relaunch
beside a skipped app waited out the whole --timeout. The result is
unchanged: reloaded true, verifiedBy app-relaunch, exit 22.

The failure text no longer says that the old runtime still answers
after a relaunch. It says that a relaunched app registers again under
the page id it had, and that nothing was watched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the first proof stops the other checks (llp/0005 §How it reloads)

Rung 1's Page.reload proof has its own window, PAGE_RELOAD_PROOF_MS
(8 s), beside the broadcast's churn window. A proof that does not come
leaves the rest of --timeout to the relaunch in --method auto. smoke's
reload uses the same rung and the same window.

jsContextReplacedAsync takes a signal that aborts the phase it lands in.
The first own target whose context is replaced stops the other checks.
A stopped check still removes its marker, on a new session when the
stop closed the old one. Its outcome is the new public value
not-needed, in runtime:reload attempts[].outcomes and in smoke's
reload.targetOutcomes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…207 changelog entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he exit code (llp/0005 §Target owner)

installStubXcrunAsync binds this worktree to the first booted simulator,
so the phone that appears during the bundle gate is another device and
rung 1 must send Page.reload to the own app only. The case checked only
that no broadcast was sent, which a run that exits before the socket
write also passes. It now asserts pageReloads() and exit 0. With the
binding off (AGENT_CLI_NO_DEVICE=1) the case fails: one broadcast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion stopped as not delivered, not unprovable (llp/0005 §What proves a reload)

A mark that got no answer (the shared deadline already spent, an abort,
a dropped connection) was `unmarkable`. When every own target ended that
way, rung 1 said no app could keep the marker, which is the Expo Go
Android case and not a spent budget. The mark now throws unless the app
answered: a CDP error answer stays `unmarkable`, anything else is
`not-delivered`, and a check stopped by another target's proof is
`not-needed`.

Unit test, not e2e: the stub dev server cannot hold the mark until the
shared deadline passes without also holding the reads around it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e.reload that kept the context (llp/0005 §How it reloads)

A delivered rung 1 (Page.reload answered, no new context) returned
failure without looking. The policy still names a bundle platform when
the skipped app is on another platform, as an Android smoke beside
another worktree's iOS app is, and smoke never relaunches, so that
bundle is the only proof left. smoke now calls observeReloadAsync for
delivered too; `unwatched: 'nothing-can-prove'` stays the path where no
bundle may count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…adcast when the read before rung 1 fails (llp/0005 §How it reloads)

prepareRungOneAsync replaced the preflight placement with its own read,
and a failed read looked like a list with nothing skipped. A probe that
failed came back as an empty list, and a killed simctl left a cloud iOS
scope with two undetermined apps; either way rung 1 broadcast to apps
the run skips. Each call also got the whole remaining budget again.

The probe and the device-tool reads (adb reverse included) now share one
deadline. A failed probe, or a tool that did not answer where the
preflight read had its answer, keeps the preflight observation, and rung
1 then sends no broadcast; Page.reload to the preflight's own targets
still runs. smoke has no earlier read, so a failed probe there sends
nothing. The e2e stub fails a /json/list read when a `targets` callback
returns null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eps the CDP-error refusal beside the lost-answer rule

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unts the skipped apps instead of naming them again (llp/0005 §How it reloads)

The run already names each skipped app on stderr and in skippedApps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as not delivered (llp/0005 §What proves a reload)

Rung 1 maps the context proof's answer-lost kind to not-delivered, so it no longer reads the reason text to tell a lost answer from an answered one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… that nothing can prove (llp/0005 §Target owner)

A cloud relaunch brings the session app back under its page id, as a local one does, so beside a skipped app of its platform no fresh target or bundle can arrive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble, one discriminated shape

The caller tests the kind instead of asking whether a session has a kind field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it failed and no app is skipped, so it never says broadcast for a run that sent none (llp/0005 §How it reloads)

rungOnePolicy takes the unread state, and runRungOneAsync switches on three mechanisms; the unread override on the prepared value is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tempt the run settled on, and a local relaunch whose wait saw nothing no longer calls the listed runtime the old one (llp/0005 §Target owner)

observeReloadAsync derives the fact from the last attempt that reloaded or reached the app, through one table keyed by method; the keepsPageId option and its two caller expressions are gone, and the failure text forks read the same key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e tool stops answering on the read before rung 1 (llp/0005 §How it reloads)

Smoke passes the last observation its own reads placed as rung 1's preflight, so the deviceFactsLost guard covers it as it covers runtime:reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e switch, so both decide "did rung 1 run" by the same rule (llp/0005 §How it reloads)

settleRungOne maps each outcome to whether a mechanism ran, its own proof and the relaunch clause; the two copies of the not-delivered-or-unprovable test and the relaunchWhy field probe are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts stopped, so it no longer shares smoke's not-needed disposition (llp/0005 §How it reloads)

The per-target outcome in attempts[].outcomes and smoke's reload.targetOutcomes is stopped; reload.disposition keeps not-needed for an app the run opened itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… same silent-tool rule as placement (llp/0005 §How it reloads)

A tool that was already silent before rung 1 loses nothing, and a tool outside the scope's platform no longer counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he macOS job

They skip off darwin, so no job ran them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y, where a local simulator can be relaunched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants