Skip to content

fix(daemon): answer a named missing session on the session, not the device - #3701

Open
thymikee wants to merge 14 commits into
mainfrom
fix/3674-named-session-not-found
Open

thymikee wants to merge 14 commits into
mainfrom
fix/3674-named-session-not-found

Conversation

@thymikee

@thymikee thymikee commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes #3674. Across 33 files, explicitly named missing sessions refuse before unconstrained device selection. Named record stop recovery reads durable manifest identity and preserves host-scoping and lease discovery context. Apple appstate carries its admission-time named-session fact through resolution; sessionless advice remains unchanged.

Validation

Published head: 7595f8226b8c06f5214b3b87bf91a3850a8ea3b0.

  • Direct gate run-bgJDRS passed at this exact commit: exit 0, exactCommit: true, matching clean fingerprints, finished 19:30 UTC. Related tests: 13,739 passed; GitHub/parked lanes remain explicitly skipped.
  • Both inline findings are resolved. Three isolated mutants each exited 1 at the intended assertion; restored tests exited 0 (3,986 tests). Fresh isolated R8 independent review found no blocking findings; it independently re-planted the mutants.
  • Parent verified this commit's repo-built CLI live proof: selectorless record stop replayed the recorded manifest's exact output after separate isolated daemon replacements on two simulators (reported walls 805/949 ms). A sessionless control returned five-way ambiguity. Missing-manifest stop refused before resolution; bare-UDID appstate returned the typed reason after resolution.
  • Simulator-set replay used the default set: positive reachability only. Lease-context carry has code/type/mutation evidence, no live lease-backed run. Raw outputs, request logs and manifests are retained; both owned simulator panels are closed and their simulators shut down; own daemons are stopped.
  • Current CI is green: 19 successes and one intentional skip; all four platform smoke lanes, Coverage and Integration passed. No failures or pending checks. Readiness label is absent. The historical Android retry succeeded; its single retry budget is used. No new retry requested.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3701/

Built to branch gh-pages at 2026-10-16 19:27 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.30 MB 5.30 MB +2.2 kB
Package (unpacked) 5.30 MB 5.30 MB +2.2 kB
Package (download) 1.59 MB 1.60 MB +985 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.1 ms 26.6 ms +0.5 ms
CLI --help 73.1 ms 74.7 ms +1.7 ms

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 25 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon/src/execution/selector-runtime-backend.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/record-runtime.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/session-appstate.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/session-appstate.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed 7c3657d. The change has a defect: Apple appstate with a named missing session and a device selector now loses its typed reason, and the Coverage check fails on it.

appstate --session sim --platform ios --device "iPhone 17 Pro" with no held session now skips the named-session refusal because of the new !hasExplicitDeviceSelector(flags) at session-appstate.ts:69. It lands on appleSessionRequiredRefusal('ios') (session-appstate-apple-session.ts:37), which has a different message and no details.reason. On iOS a device selector cannot be answered without a session, so the exemption gains nothing there and only drops the typed reason. An Apple caller who names a lost session plus a device gets an untyped message instead of named_session_not_found, the reason this PR documents as the one to branch on. The existing test at session-appstate-input.test.ts:218-243 pins the named-session answer for this exact input and is the failing check. The rule should be: a named missing session answers named_session_not_found unless the command can serve the stated selector without a session. In appstate, please apply the identity-selector exemption only when the platform is not Apple (!isApplePlatform(flags.platform)), so that test and the new Android identity test both pass. Please do not rewrite the old test to match the new message.

This also changes what snapshot, wait, get, is, record and close answer on a device, but the PR body only cites fixture tests and a historical revert result at explicit-session-resolution.ts:50. No live run shows the issue's one-command repro, so nothing proves the real host listing is skipped. With two iOS simulators booted and AGENT_DEVICE_STATE_DIR=$(mktemp -d), please run agent-device snapshot --session never-opened --json and show SESSION_NOT_FOUND with details.reason named_session_not_found, well under the 3 s listing time. Then run agent-device snapshot --session never-opened --udid <A> and show that it resolves simulator A and not AMBIGUOUS_MATCH.

Not blocking, take or leave: the guard at explicit-session-resolution.ts:59 exempts any hasDeviceSelectionInput (including a bare --platform), while appstate exempts only hasExplicitDeviceSelector, so snapshot --session lost --platform ios with two simulators booted still lists devices and answers AMBIGUOUS_MATCH; one predicate owned in device-selector-intent.ts and documented in commands.md would fix both, and the control-flow and review-history comments in snapshot-session.ts, batch.ts, selector-runtime-backend.ts and record-runtime.ts can go once the guard has one owner.

Could the refusal live in one place in the router instead, after the locked binding resolves existingRef and before route dispatch? The conditions would be: no held session, namesSessionExplicitly(req), no device-target selector, and the command not declared as one that may run with a missing named session (open, replay, batch, lease, session list, devices, sessionless close). That would replace the four dynamic-import guards and the local appstate variant. noActiveSessionError and the requireSessionOrExplicitSelector answers would then carry named_session_not_found when a session was named, giving one typed shape for one state. It would need a request-policy trait in daemon-command-registry.ts declaring which commands may run with a named but missing session, so the router reads a declaration instead of each handler guarding.

The cubic-dev-ai thread on the router test still applies: request-router-named-session.test.ts:112-126 asserts the code and reason but not listings.count, so it would not catch a listing added before the guard.

The Coverage failure comes from this diff, not from a flake. I read the log and the head code and did not run the tests locally. I also did not check which clients send --platform on every read, so I cannot size how many callers hit the bare-platform gap. The revert evidence in the PR body is the author's earlier claim; I judged it by reading the assertions. There are no conflicts. Before merge, restore named_session_not_found for Apple appstate with a device selector so the Coverage test passes, then attach the live repro with two booted simulators.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the update. The two simple-case fixes look right, but the Apple carve-out in appstate still misses a route, so the change is not ready at 81b22ab.

The check at packages/daemon/src/sessions/handlers/session-appstate.ts:68 only reads the --platform flag. Take appstate --session lost --udid <ios-sim-udid>, or --device "iPhone 17 Pro", with no --platform. It passes the guard, then resolveCommandDevice at :102 lists host devices and picks the simulator. queryDeviceRuntimeAppState (session-appstate-device-runtime.ts:28) then returns appleSessionRequiredRefusal('ios'), a SESSION_NOT_FOUND with no details.reason. So a caller with a lost session gets a device listing and an untyped refusal, not named_session_not_found. That also contradicts the commands.md line this PR adds. The rule should be: a request that names a missing session gets named_session_not_found unless the command can serve the stated target without a session. For appstate, only the resolved device knows that. I suggest appleSessionRequiredRefusal, or the call after resolution at :102, returns the named-session refusal when there is no session and namesSessionExplicitly(req) is true. Please add a test with { session: 'sim', udid: <ios udid> } and no platform. I read this from the head code and did not confirm that no CLI or client path fills in --platform first, so I rate it likely, not certain.

The live repro in the PR body is still pending, and this change affects snapshot, wait, get, is, record, close and appstate on real hosts (explicit-session-resolution.ts:47). Please boot two iOS simulators and set AGENT_DEVICE_STATE_DIR=$(mktemp -d). Then run agent-device snapshot --session never-opened --json. It should show SESSION_NOT_FOUND with details.reason named_session_not_found, dispatched no, and no listing delay (well under the ~3 s listing time). Run agent-device snapshot --session never-opened --udid <A> --json. It should resolve simulator A, not answer AMBIGUOUS_MATCH. Run agent-device appstate --session never-opened --udid <A> --json. It should answer named_session_not_found. I did not run the tests or the daemon.

The earlier question is still open and has no reply: could the router refuse once, after the binding resolves, when there is no held session, namesSessionExplicitly(req) is true, there is no identity selector, and the command lacks a "runs with a missing named session" trait in daemon-command-registry.ts (open, replay, batch, lease, session list, devices, sessionless close)? Could every session-required refusal (noActiveSessionError, requireSessionOrExplicitSelector, appleSessionRequiredRefusal) also carry named_session_not_found through one shared builder that takes the request's session provenance? That would remove the four dynamic-import guards and the appstate special case, and it would close the --udid gap by construction. The appstate-only carve-out leaves that gap open.

On the open threads: the P3 thread at #3701 (comment) is addressed, because request-router-named-session.test.ts:130 now asserts listings.count is 0 for record start, so you can resolve it. The P3 thread at #3701 (comment) still applies: session-appstate-input.test.ts:242-244 checks only the self-reported dispatched "no", so nothing pins that resolution was skipped, which is the route above.

The code does not pass yet because of the appstate route. Smoke Tests were still running when I checked, so I cannot attribute any CI result yet. Their route overlaps this diff (session resolution in snapshot-session.ts and selector-runtime-backend.ts), so a smoke failure there should be treated as real. Before merge, make Apple appstate answer named_session_not_found for --udid or --device with no --platform, then attach the live two-simulator run.

@thymikee

Copy link
Copy Markdown
Member Author

Both requirements are addressed at c6be0ce23 (one test-only commit over 81b22ab3c).

Apple appstate typed reason. Restored in 81b22ab3c as you specified — the identity-selector exemption in session-appstate.ts now reads (!hasExplicitDeviceSelector(flags) || isApplePlatform(platform)), so an Apple caller with a named missing session plus a device selector keeps named_session_not_found, and the existing session-appstate-input.test.ts test was kept (only typed assertions added, message pins unchanged). The Android/HarmonyOS identity lane is unchanged and pinned by the emulator-5554 positive test. Live at this head: appstate --session never-opened --platform ios --device "iPhone 17 Pro" → SESSION_NOT_FOUND, details { reason: named_session_not_found, dispatched: "no", session: "never-opened" }.

Live repro (repo-built node bin/agent-device.mjs at c6be0ce23, version 0.21.29-dev; the homebrew 0.21.11 on PATH was not used; AGENT_DEVICE_STATE_DIR=$(mktemp -d), no device boot/reset, five booted iOS sims + one booted physical found on the host):

  • snapshot --session never-opened --json → SESSION_NOT_FOUND / named_session_not_found / dispatched: "no" in 390 ms cold (daemon spawned by the call), 180 ms warm, against a 2,388 ms devices --platform ios listing of the same pool. The request log for the refusal contains only request_start and request_failed — no resolve_target_device phase, so the host listing was really skipped.
  • snapshot --session never-opened --udid <A> → not AMBIGUOUS_MATCH: the request log shows resolve_target_device (276 ms, cacheHit=false) resolving simulator A, then the device's own ios_app_session_required. Cheaper end-to-end proof on the same routing lane: apps --session never-opened --udid <A> succeeds (832 ms, lists A's installed apps).
  • Control: sessionless snapshot --json still lists and answers AMBIGUOUS_MATCH (5 devices), so the intentional sessionless path is preserved.

Take-or-leave items. The single-predicate/bare-platform gap and the router-central guard with a daemon-command-registry request-policy trait are real directions; both are refusal-taxonomy changes across every command, so we recorded them as follow-ups rather than widening this PR — happy to see a follow-up issue filed for the device-selector-intent.ts predicate. The remaining cubic thread (listings.count for record start) is pinned since 81b22ab3c and was resolved with that citation; the appstate self-reported-dispatched thread is fixed at c6be0ce23 with a not.toHaveBeenCalled() pin whose mutation evidence is in the last commit's message and the PR body.

@thymikee

Copy link
Copy Markdown
Member Author

I reviewed c6be0ce. The 81b22ab finding on the selector-only route is still open, because the new commit changed only a test. Checks are pending: both Smoke Tests jobs are queued or running. They cover the session resolution this diff changes, so treat a failure there as related until shown otherwise. There are no conflicts. The earlier review is here: #3701 (comment)

appstate --session lost --udid <ios-sim-udid> (or --device "iPhone 17 Pro") with no --platform still skips the named-session refusal. session-appstate.ts:68 sees an explicit device selector and no Apple platform. resolveCommandDevice then lists host devices and picks the simulator. queryDeviceRuntimeAppState (session-appstate-device-runtime.ts:28) answers with appleSessionRequiredRefusal('ios'), a SESSION_NOT_FOUND without details.reason. A caller whose Apple session was lost pays for a device listing and gets an untyped refusal instead of named_session_not_found. That does not match the commands.md line this PR adds or the typed-reason contract in #3674. The rule: when a request names a session the daemon does not hold, and the resolved target can only be served from a session, the refusal carries named_session_not_found. Only the resolved device knows if it is Apple, so should queryDeviceRuntimeAppState (or appleSessionRequiredRefusal, given the request's session provenance) return appStateSessionNotFoundResponse when there is no session and the request names one explicitly? Please add a test with {session:'sim', udid:<ios udid>} and no platform that asserts details.reason. I read this from the head code and did not run it. Does any CLI or client path fill in --platform from --udid or --device before the daemon? If so, tell me where.

The live run does not cover that route yet. The author's proof and the pinned test both pass --platform ios, which was refused before resolution and already worked. After the fix, please run agent-device appstate --session never-opened --udid <A> --json with no --platform and two or more booted iOS simulators. It should return SESSION_NOT_FOUND with details.reason named_session_not_found. Please also say whether the request log shows a resolve_target_device phase.

The Cubic thread on the listings.count assertion in request-router-named-session.test.ts is fixed at this head, so please resolve it: #3701 (comment). The thread on session-appstate-input.test.ts:245 is also fixed at this head, but it covers only the --platform ios input, so please resolve it after the new test lands: #3701 (comment)

Before merge, appstate --session <missing> --udid <ios udid> with no --platform must answer named_session_not_found, and the live run above must be attached.

@thymikee
thymikee force-pushed the fix/3674-named-session-not-found branch from c6be0ce to e18e053 Compare October 16, 2026 12:48

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon/src/sessions/handlers/record-runtime.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
@thymikee

Copy link
Copy Markdown
Member Author

Round-c6 review consumed; both findings addressed in bffd99a.

F1 (P2, record stop manifest recovery): accepted. The guard now runs after resolveRecordPlan and fires only on plan.kind === 'start', still before the app-scope refusal. record stop reaches the durable manifest again: its recovery proves the manifest's session and device (assertManifestBelongsToRequest) before serving, and the manifest path derives from the named session, so the named caller is the only one that has this lane at all. Both directions pinned in record-runtime-stop-recovery.test.ts: after retiring the session in the store, the stop replays the completed export (fails with the guard stop-wide: expected ok:true, received the named-session refusal), and a start after the same loss still refuses with named_session_not_found (fails when the guard is removed). Exact-head pnpm check:affected --run --head bffd99ad9 --json: exit 0, exactCommit: true.

F2 (P3, reasonless SESSION_NOT_FOUND family): accepted as doc-only. The named_session_not_found row in commands.md now states that the session-required family answers the same event without a reason and tells callers to treat a reasonless SESSION_NOT_FOUND on a named session as this one. Adding the reason to those seams instead would change refusal shapes for ~9 call sites the issue does not scope.

Rejected-lead adjudication from your report matches the owner's dispositions; nothing reopened.

…evice (#3674)

A command that names a session this daemon does not hold used to fall
through to resolveTargetDevice({}) — the session-attached command carries
no device selectors, so the device was picked from the host inventory.
With two devices booted the caller met AMBIGUOUS_MATCH telling it to
select a device; with one it met an app-session error. Either way the
answer depended on how many devices happened to be up and sent the
caller after a device selector when what it lost was its session.

Refuse at the seam instead: requireNamedSessionHoldsDevice throws
SESSION_NOT_FOUND with typed reason named_session_not_found and
dispatched=no, wired into every resolution that would otherwise choose a
device (snapshot, selector runtime, recording, sessionless close). A
request that never named a session stays sessionless by design, and one
that names a device selector as well keeps its stated target.

Provenance is traced, not inferred: namesSessionExplicitly reads
meta.sessionExplicit or a non-blank flags.session, replacing the two
copies of hasExplicitSessionFlag. It lives in @agent-device/kernel/
contracts, beside the meta field it reads, so every consumer already
evaluates it; the guard module reaches the hot seams through a
function-scoped import, keeping the eager-closure gate at parity
(ADR-0019 loading shape). The refusal is pinned by a router regression
test, unit tests, and a new daemon.route.named-session-refusal
dispatch-disclosure row.
#3674)

The batch runner stamped flags.session on every step so nested invokes route to the
parent's resolved address. The wire `session` field already does that; the flag is a
provenance carrier, and the new named-session refusal reads it. An unconditional stamp
therefore made a sessionless batch look caller-addressed and met SESSION_NOT_FOUND at
the daemon — an error no caller of the batch surface asked for, with advice ("omit
--session") the surface does not even offer. The stamp now follows the same traced
provenance as every other carrier: meta.sessionExplicit or a caller-set step flag.

Also from the same review: the named-session disclosure row's driver now pins the
producer's own code and typed reason, so a reverted guard cannot satisfy it with the
device-selection error it replaced; commands.md states which pre-dispatch refusals
answer session_or_device_selector_required even for a named-but-unheld session.
…every seam

Round-2 review found #3674's refusal reaching some callers as an untyped
event. `get`/`is` answered a missing session with the generic "no active
session" error before the provenance guard could run; `record start` let
its app-scope INVALID_ARGS refusal win over a session that was gone
altogether; `appstate` carried no typed details and refused requests
that had also stated a concrete device. The refusal details now have one
producer, `namedSessionNotFoundDetails` beside its sibling builders, that
the guard's AppError and `appstate`'s platform-aware response both build
from; each seam orders the guard before its own missing-session refusal;
`appstate` preserves a request naming an identity selector, matching the
resolution guard. The guard module's function-scoped import is confined
to the missing-session branch it can fire on: that keeps the eager-
closure gate at parity and removes a first-time transform cost from the
held-session wait path, which under coverage instrumentation alone
overran `wait-runtime.test.ts`'s fake-timer budget at the unconditional
form (3/3 deterministic failures restored, 3/3 passes at this form).
`namesSessionExplicitly` now accepts the generic flag-record shape the
batch runner carries, fixing the TS2345 the batch call site raised.
…selector states

appstate's identity-selector exception over-preserved: a named missing session
with an explicit Apple device fell through to the session-required refusal, a
route with no sessionless answer. Keep the typed named-session refusal wherever
the command cannot serve the stated target without a session, and let only
non-Apple identity selectors keep their target.

Also pins the appstate refusal's typed reason/session/dispatched details, adds
the record seam's no-inventory assertion, and trims seam comments to their
invariants.
The appstate refusal reported dispatched: 'no', a value it writes itself, so the
test would still pass if the handler resolved a device before answering it. The
new assertion pins the behavior instead: resolving the target device never runs
on the named-session path. Reordering the guard to resolve-then-refuse with the
same code, message, reason and dispatched fields passes every other pin and
fails only this one.
…on addresses (#3674)

The named-session guard covered every record action, so after a daemon restart — the
exact state #3674 serves — `record stop --session qa` was refused before the on-disk
`screen-recording.resource.json` was read, killing the manifest recovery that `help
record` and commands.md promise. That recovery is only reachable through a named
session: the manifest path derives from the session address, and the lane proves the
manifest's session and device before serving. Start is different: it can only be
answered on a chosen device, so it keeps the pre-selection refusal. Order the guard on
the resolved plan (`plan.kind === 'start'`, still before the app-scope refusal) and pin
both directions: the replayed completed export survives store loss, and a start after
the same loss still refuses on the session. Reverting the guard fails both start pins;
keeping it stop-wide fails the replay pin.

Also complete the refusal taxonomy in commands.md: the session-required family answers
the same event as a reasonless SESSION_NOT_FOUND, so document that branch.
@thymikee
thymikee force-pushed the fix/3674-named-session-not-found branch from bffd99a to 309a3be Compare October 16, 2026 13:39
@thymikee

Copy link
Copy Markdown
Member Author

Round-bffd99ad9 review consumed: verdict READY (F1/F2 accepted-fixed; no new triggering findings; rejects recorded). Full report: /tmp/3674-review-bffd99ad9-final-r1.md. The branch was then rebased onto origin/main 87fe3ea (content byte-identical, 44/44 hunks); fresh final review at 309a3be is running now.

The two new review threads at this head are answered on-thread: the record stop multi-boot AMBIGUOUS_MATCH thread is rejected as pre-existing base behavior (this branch's only record-runtime hunk is the start-gated guard; the recovery lane is unchanged from origin/main), with the manifest-first fix shape offered as a follow-up issue if a maintainer wants it separately; the docs thread about reasonless SESSION_NOT_FOUND implying a dispatch guarantee is accepted as a doc-only precision, queued into the next batched commit alongside any accepted findings from the running review, then one push, one new exact-head gate receipt, and one fresh final review at that SHA.

Current head 309a3be: exact-head check:affected exit 0 with exactCommit true (receipt run-qDhNlT); focused suites 54/54; live repro re-run at this build (typed refusal 388 ms no resolve phase; apps --udid A succeeds; sessionless control still AMBIGUOUS_MATCH). CI at this head is still in flight — no green claim.

@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the update. At 309a3be the appstate and test-assertion threads are partly fixed, but record stop after a daemon restart still resolves a device with no constraint, so this is not ready to merge yet.

Fixed since c6be0ce: the listing-count assertion in request-router-named-session.test.ts and the "not called" assertion in session-appstate-input.test.ts (the second covers only the --platform ios input).

Still open, first: in record-runtime.ts:158 the named-session guard runs only when plan.kind === 'start'. After a daemon restart, record stop --session qa finds no held session, and resolveRecordingSession calls resolveTargetDevice(req.flags) with no selector. With two or more booted devices this answers AMBIGUOUS_MATCH before the manifest is read, which is the #3674 symptom. With one device it answers INVALID_ARGS "no active recording" (no manifest), or runtime-contract-invalid (device differs from the manifest's). A caller whose daemon was replaced cannot recover its recording without a selector. The new replay test mocks resolveTargetDevice to return the manifest's device, so it cannot hit this. The rule: a request that names a session the daemon does not hold, and has no device selector, never reaches resolveTargetDevice with its own flags. Apply it in resolveRecordingSession, the only record site that calls it. With no ref and no selector, stop-recovery reads the manifest and resolves its envelope.device exactly. With no decodable manifest, or for start, it throws namedSessionNotFoundError. Please test with a resolveTargetDevice double that throws AMBIGUOUS_MATCH as production does, and assert that stop serves the export and that stop with no manifest answers named_session_not_found.

Still open, second (unchanged from c6be0ce): appstate --session lost --udid <ios udid> with no --platform passes the guard at session-appstate.ts:68. resolveCommandDevice then lists devices, and session-appstate-device-runtime.ts:29 returns appleSessionRequiredRefusal('ios'), a SESSION_NOT_FOUND with no reason. commands.md:488 says Apple appstate refuses with named_session_not_found "even with an explicit device", so the documented reason does not hold on this route, which also pays for a device listing. Should queryDeviceRuntimeAppState return appStateSessionNotFoundResponse for an Apple device when the session is missing and namesSessionExplicitly(req)? Please add a {session, udid} test with no platform that asserts details.reason. Narrowing the doc parenthetical to --platform apple|ios|macos also works.

Could the f1 guard live inside resolveRecordingSession instead of a plan.kind carve-out in front of it? The stop lane would take its device from the manifest envelope, every other case without a held session would refuse, and every record caller would follow one rule at the only resolveTargetDevice call in the record handler. The wider per-handler-guard question was settled earlier and stays closed. This reopens it only for record.

Not blocking, take or leave: the reasonless SESSION_NOT_FOUND from noActiveSessionError sits in commands.md:488 under the lead-in that says these refusals carry dispatched: no and a typed reason, so please move it out of that list or say it carries no details.

Open review threads: the record stop device resolution thread and the reasonless refusal docs thread still apply. These two no longer apply, so please resolve them: listing count assertion is fixed at head, and appstate not-called assertion is fixed at head for the --platform ios input.

I ran no tests, and my notes on which tests would fail without each fix come from reading the code. I did not check whether any CLI or client path fills in --platform from --udid before the daemon, which would make the appstate case unreachable.

No conflicts are known. CI is pending: Smoke Tests is still running. Smoke runs CLI flows through daemon session resolution, which this diff changes, so please treat a failure there as related until shown otherwise.

Before merge, record stop for a named session the daemon no longer holds must take its device from the manifest, or refuse with named_session_not_found, before any unconstrained device resolution. Then please run this live on a host with two or more booted devices and no selector: record start --session qa, replace or restart the daemon, then record stop --session qa --json. It must return recording:stopped with the outPath, and the request log must show no unconstrained resolve phase. Then record stop --session never-opened --json must answer SESSION_NOT_FOUND with details.reason named_session_not_found. For appstate, please run the check requested at c6be0ce: appstate --session never-opened --udid <A> --json, no --platform, with two or more iOS simulators, answering named_session_not_found.

…tch (#3674)

The named-session row sits under the retry legend's dispatched: no umbrella, but the
session-required family answers with no details at all. Reading the cause equivalence
under that umbrella would promise a non-dispatch those responses never make, inviting a
blind retry. The row now says the branch is on the missing-session cause only, and the
missing dispatched keeps its usual no-guarantee reading.
@thymikee

Copy link
Copy Markdown
Member Author

Two defects from the earlier review (309a3be) are still open at 16f9460. The docs rewrite fixes the cubic-dev-ai threads on the commands.md wording and the two test assertions. The code is unchanged.

The record stop --session qa path is still not fixed. After a daemon restart there is no held ref and no selector, so the guard at line 109 (start only) is skipped, and resolveRecordingSession calls resolveTargetDevice(req.flags) before the manifest is read (record-runtime.ts:158). With two or more booted devices this answers AMBIGUOUS_MATCH. With one device that is not the manifest's device, it answers a runtime-contract error. A caller whose daemon was replaced gets a device failure instead of its recording or named_session_not_found. This is the #3674 symptom on the record route, and the earlier review made fixing it a condition for merge. The rule: a request that names a session the daemon does not hold, and passes no device selector, never reaches resolveTargetDevice with its own flags. Please apply it inside resolveRecordingSession, the only resolveTargetDevice call in the record handler. With no ref and no selector, stop-recovery should take the device from the decoded manifest's envelope.device. In every other no-ref case it should throw namedSessionNotFoundError. Please test with a resolveTargetDevice double that throws AMBIGUOUS_MATCH, as production does.

appstate --session lost --udid <ios-udid> with no --platform also still returns an untyped refusal. It passes the check at session-appstate.ts:65-69, because the request has an explicit selector and no Apple platform. The handler then resolves the device, and queryDeviceRuntimeAppState returns appleSessionRequiredRefusal('ios') (session-appstate-device-runtime.ts:29). That is a SESSION_NOT_FOUND with no details.reason, while commands.md:488 says Apple appstate refuses with named_session_not_found "even with an explicit device". The caller also pays for a device listing first. The rule: an iOS or macOS device with no session and namesSessionExplicitly(req) returns appStateSessionNotFoundResponse. Please add a test with {session, udid} and no platform that asserts details.reason. Or, if you prefer, narrow the doc parenthetical to --platform apple|ios|macos.

Still open: the record-runtime resolveTargetDevice thread applies and is the first defect above. The other three inline threads are fixed at this head and you can resolve them: the commands.md:488 scoping, the listing-count assertion, and the resolver-not-called assertion. The last one covers --platform ios only. The --udid case without a platform is the second defect.

I ran no tests, and I did not verify the exact-head check:affected receipt. Both findings come from reading the head code. I did not check whether any CLI or client path fills in --platform from --udid before the daemon, which would make the second one unreachable. I also did not check whether the "screenshot-style generic commands" wording matches every generic command that returns noActiveSessionError.

CI is pending. Both Smoke Tests jobs are queued with no log yet. Smoke runs CLI flows through daemon session resolution, which commits 1-6 change, so treat a failure there as related until shown otherwise.

Before merge, record stop for a named session the daemon does not hold must take its device from the manifest, or refuse with named_session_not_found, before any unconstrained resolveTargetDevice. Apple appstate with --udid and no platform must carry the typed reason. Then please attach two live runs. First, on a host with two or more booted devices, open a session qa, run record start, restart the daemon, then run agent-device record stop --session qa --json with no selector. It must return the recording, or named_session_not_found when no manifest exists, and the request log must show no resolve_target_device phase. Second, agent-device appstate --session never-opened --udid <ios-sim-udid> --json with no --platform must return SESSION_NOT_FOUND with details.reason named_session_not_found.

…a host sweep (#3674)

A named session the daemon no longer holds was still answered on an unconstrained
resolveTargetDevice for record stop, so a multi-boot host replied AMBIGUOUS_MATCH
before the durable manifest — the one thing that can answer that stop — was read.
The named-session rule now lives at the handler's only device selection: a request
addressed to a session takes its device from a decodable manifest envelope, and a
start, or a stop with nothing decodable to recover from, refuses with the typed
named-session error. Selector and sessionless lanes keep their targets, and the
manifest's own session and device proofs still fail closed.

Co-Authored-Worked-On-By: Claude
…ce route (#3674)

`appstate --session lost --udid <ios>` without `--platform` passed the admission
guard, listed the host, resolved onto an Apple device, and answered the reasonless
session-required refusal — contradicting the commands.md promise and paying for a
listing the typed answer makes unnecessary. The device-runtime lane now answers a
named-missing session with the same typed refusal as every other seam; a request
that named no session keeps the plain session-required advice, and the non-Apple
identity-selector lane is untouched. The refusal builder moved next to its sibling
Apple refusals so both routes share one wire shape.

Co-Authored-Worked-On-By: Claude
…el (#3674)

The r4 sentence claimed the family answers with no details at all and voided the
dispatched guarantee; the disclosure layer stamps click/fill/screenshot-style reads
dispatched: no and type/swipe-style mutations dispatched: unknown, so the claim was
false for every member it named and wrong in both reading directions. The row now
states the fact at the details.reason level, keeps the usual dispatched reading,
and treats only absence (logs/network/audio) as no guarantee.

Co-Authored-Worked-On-By: Claude

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon/src/sessions/handlers/__tests__/session-appstate.test.ts Outdated
Comment thread packages/capture-kit/src/capture-admission/screen-recording-stop-recovery.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/session-appstate-device-runtime.ts Outdated
…admission (#3674)

The device-runtime lane re-read the session store and re-ran the naming check after
awaiting device resolution, so a store change mid-await could move the request into
a different error family than the one it was admitted into, and the classifier was
spelled out twice. Admission now carries one typed namedSessionMissing fact, decided
from the same store read as the admission session itself. A bare --udid still
resolves its stated target first and answers the typed reason from the resolved
device — pinned by asserting the resolution call, not just the payload — and the
store-changes-during-resolution case pins the snapshot semantics. The manifest-first
docstrings say what actually happens: the manifest's identity is re-resolved against
the host for the live device fields, with the session/device assertions failing
closed.

Co-Authored-Worked-On-By: Claude

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon/src/sessions/handlers/record-runtime.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/session-appstate-device-runtime.ts Outdated
…two doc claims (#3674)

The bare-udid pin built its runtime owner with `ios`, which is a public selector, not
a Platform — typecheck TS2345 at the lint-package gate. The fixture device's own
family, `apple`, is the value the owner takes. Alongside it, two docstrings said more
than the code does: the manifest lane asks the host only for the manifest's device
where the family has an identity flag (families without one keep the request's own
selection, proven by the manifest assertion), and the admission-carried fact keeps the
refusal stable once a device resolves — resolution itself can still fail first with
the device's own error.

Co-Authored-Worked-On-By: Claude
@thymikee

Copy link
Copy Markdown
Member Author

The earlier findings at 309a3be are mostly fixed at 2d3838f: the unheld named stop now reads the manifest before it resolves, and the docs, the test and the comments now match the code. One defect remains in the new stop recovery, so this needs changes.

resolveManifestDevice builds a fresh selector from the manifest family and identity, and it drops the request's device-scope flags (iosSimulatorDeviceSet, androidDeviceAllowlist, leaseId, leaseProvider, deviceKey, clientId). hasDeviceSelectionInput does not count these flags, so the named-session path still applies. Take record stop --session qa --ios-simulator-device-set <set> after a daemon restart. Before this change it resolved from req.flags and saw the set's inventory. Now it lists the default simulator set, the scoped UDID is missing, and the stop answers DEVICE_NOT_FOUND. Leased devices likely lose their provider inventory the same way. The rule: the manifest re-resolution keeps every request scope field and adds only the manifest's family and identity, in every branch of that function, including the no-identity fallback. session-device-resolution.ts:67-72 already does this for iosSimulatorDeviceSet (https://github.com/callstack/agent-device/blob/2d3838f/packages/daemon/src/sessions/handlers/record-runtime.ts#L219). Please add a stop-recovery test with iosSimulatorDeviceSet in the flags, and make the test double refuse a call that omits it. I read the code and ran no tests or daemon. I did not confirm how often clients send these flags per request.

Not blocking, and you can take or leave these: for a family with no identity flag (web, linux), line 222 drops the selector and calls resolveTargetDevice(params.req.flags), and the reason given does not hold for web because platform-web supports app-scope recording, which writes a manifest, so passing the scope flags plus platform: recorded.family there follows the same rule; namesSessionWithoutTarget repeats the namesSessionExplicitly && !hasDeviceSelectionInput predicate from requireNamedSessionHoldsDevice, so exporting it from explicit-session-resolution.ts would give one source; and commands.md:488 tells clients to read the cause from the message for a reasonless SESSION_NOT_FOUND, so it could say instead that this form has no stable discriminator beyond the code and the named session.

The live run described in the PR body for the 633115a build has no raw output posted. Please post it for two or more booted devices with no selector: record start --session qa, restart the daemon, then record stop --session qa --json. It must return recording:stopped with outPath, and the request log must show a resolve_target_device phase that carries the manifest's udid or serial. Then record stop --session never-opened --json must return SESSION_NOT_FOUND with details.reason named_session_not_found. Then appstate --session never-opened --udid <A> --json with no --platform must return named_session_not_found. After the fix above, repeat the stop with --ios-simulator-device-set <set> naming a scoped simulator.

On the other threads, the cubic-dev-ai threads on the manifest read order (#3701 (comment)), the commands.md split (#3701 (comment)), the appstate test (#3701 (comment)), the recovery docblock (#3701 (comment)), the admission decision (#3701 (comment)) and the resolution comment (#3701 (comment)) are fixed at this commit, so you can resolve them. The web thread at #3701 (comment) still applies, and it is covered by the note above.

Smoke Tests is still running with no failure log yet. It drives snapshot, selector and record routes through daemon session resolution, which this PR changes, so please treat any failure there as related until shown otherwise. Before merge, the manifest re-resolution must keep the request's scope flags, and the raw live stop output must be posted.

…overy lane

Manifest re-resolution dropped iosSimulatorDeviceSet and androidDeviceAllowlist,
regressing recovery scoped to a simulator set or allowlist, and the flagless-
family lane still swept the host from the request's flags. The selector now
carries the manifest's family always, the identity flag where the family has
one, and the request's host-scoping selectors; no named recovery lane sweeps.

Pins: recovery preserves the allowlist (drop-carry mutant caught), flagless
recovery resolves under the manifest family (req.flags-sweep mutant caught),
sessionless appstate on an Apple device keeps the plain session-required advice
(always-typed mutant caught). Trims the restated fail-closed invariant down to
its owning site.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon/src/sessions/handlers/record-runtime.ts
Manifest re-resolution forwarded the host-scoping selectors but dropped the
request's lease/provider-discovery fields (leaseId, leaseProvider, deviceKey,
clientId), which flow into provider discovery — a leased named record stop
resolved outside its lease, unlike base, which passed the whole request flags.
The non-identity discovery context now has one owning read in
device-selector-intent next to the selector classification, and
resolveManifestDevice forwards it.

Pins: apple-manifest recovery carries iosSimulatorDeviceSet (the surviving
drop-spread mutant; deleting either spread line now fails a test), recovery
carries the lease context (drop-context mutant caught), and the sessionless
plain-advice test asserts the resolver call on the stated udid so a refusal
that skips resolving an invalid UDID fails it. The record fixture builds its
durable envelope with the same kernel deviceIdentity() the transports use, so
an apple manifest is decodable and every pin asserts a healthy start/stop and
a decoded manifest before exercising recovery.

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.

A session lost to a daemon takeover ("Replacing daemon … unreachable") answers the next request with AMBIGUOUS_MATCH when two simulators are booted

1 participant