A session lost to a daemon takeover ("Replacing daemon … unreachable") answers the next request with AMBIGUOUS_MATCH when two simulators are booted
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 25/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- ios, nodejs, typescript
Línea de trabajo
Start in src/daemon/snapshot-session.ts, where resolveSessionDevice falls back to resolveTargetDevice({}) when the named session is missing, then check resolveCommandDevice in src/daemon/session-device-resolution.ts. Compare with session-close.ts, which already returns SESSION_NOT_FOUND. Reproduce with two booted simulators and a session the daemon does not hold. Done means a named missing session with no device selector returns SESSION_NOT_FOUND, and the daemon-takeover notice in daemon-client-lifecycle.ts gains a reason. The issue leaves the fix direction open, so confirm it with a maintainer first.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
Twice today an automated iOS run lost its session in the middle of a test. The client printed Replacing daemon (pid N, v0.21.25) in ~/.agent-device: unreachable, up to three times in one test. The replacement daemon held no sessions. The next snapshot for that session then failed after 3.3 s with AMBIGUOUS_MATCH: 2 devices match this request equally, because two iOS simulators were booted. The request named a session, but the daemon had none, so it ran device selection with no constraints.
I see two problems, one small and one larger:
- A request that names a session the daemon does not hold should say so.
closealready answersSESSION_NOT_FOUND: No active session.snapshotinstead falls through toresolveTargetDevice({}), so the answer depends on how many devices are booted. With one device booted you get an app-session error (see #1596). With two you get an error that tells you to pick a device, which hides the real cause and sends the caller after the wrong fix. This reproduces with one command (below). - A takeover that drops every session gives no reason. The notice says only
unreachable. It does not say if the pid was dead, if a probe timed out, or how many sessions it threw away. The replaced daemon's log is gone afterward. I could not tell if the daemon died or if it missed its probes (see "What I could not establish").
I know #1596 treats SESSION_NOT_FOUND after a replace as expected, since a new daemon has no sessions. I agree about the error. My point is the wrong error with two devices, and that the replace itself looks avoidable or at least explainable.
Environment
- agent-device 0.21.25 (the same code is on
mainat 35c21b6, 2026-10-15; see "Where in the code") - Driven through the Node client:
@e2e-dev/mobile0.12.0 undere2e0.20.0, which callscreateAgentDeviceClient({ session: 'e2e-ios-0' }), one worker, default state dir~/.agent-device, transportauto(the daemon registered a socket port, no HTTP port) - macOS 27.0.1 (26A434), arm64, Node v26.11.1, Xcode 27.0 (27A266a)
- Two iOS 27.0 simulators booted:
TinyConcepts E2E(the one the run drives) andPerf iPhone 17 - The engine is configured with
mobile({ platform: 'ios', device: <E2E_DEVICE> }). It passes the device onboot,open,installandpermission. Reads such assnapshotpass only{ interactiveOnly }and rely on the session to hold the device. (@e2e-dev/mobiledist/surface.js:selection()at line 296,capture()at line 631.)
What happened
Times are UTC from ~/.agent-device/sessions/e2e-ios-0/events.ndjson. The run printed Replacing daemon (pid 52888 / 53707 / 55890, v0.21.25) in /Users/…/.agent-device: unreachable three times during its first test, which then failed with snapshot failed: Failed to communicate with daemon. The other two tests of that run passed.
13:08:30.9 boot TinyConcepts E2E ok test 1 starts
13:08:42.5 open com.anyconcept.app ok 11 s after the boot
13:08:46 snapshot x4 ok 5.0 s, 1.3 s, 1.3 s, 1.3 s
13:08:59.4 last good snapshot finishes
13:09:05.5 snapshot f40644a2477f0603 starts 6.1 s later
13:09:06.7 close 7479a0272ecdf229 starts test teardown
13:09:08.86 snapshot f40644… -> AMBIGUOUS_MATCH 3356 ms (resolve_target_device 3329 ms)
13:09:08.86 close 7479a0… -> SESSION_NOT_FOUND "No active session"
The daemon wrote the request log of the failing snapshot (paths shortened):
{"ts":"2026-10-15T13:09:05.504Z","level":"info","phase":"request_start","session":"e2e-ios-0","requestId":"f40644a2477f0603","command":"snapshot","data":{"publicSession":"e2e-ios-0","effectiveSession":"e2e-ios-0","command":"snapshot"}}
{"ts":"2026-10-15T13:09:08.849Z","level":"error","phase":"resolve_target_device","session":"e2e-ios-0","requestId":"f40644a2477f0603","command":"snapshot","durationMs":3329,"data":{"cacheHit":false,"error":"2 devices match this request equally; select one explicitly."}}
{"ts":"2026-10-15T13:09:08.860Z","level":"error","phase":"request_failed","session":"e2e-ios-0","requestId":"f40644a2477f0603","command":"snapshot","data":{"code":"AMBIGUOUS_MATCH","message":"2 devices match this request equally; select one explicitly."}}
An earlier run, 13:06 to 13:08, shows the loss more sharply. In its second test, open finished OK at 13:06:34.2. At 13:06:35.4, 1.2 s later, a snapshot for the same session failed with AMBIGUOUS_MATCH (3471 ms), and again twice more at 13:06:38.8 and 13:06:40.7. A session that existed 1.2 s earlier was gone. That run failed all three tests: two with AMBIGUOUS_MATCH, one with Failed to communicate with daemon.
Counts for the day, from the same events file: 7 AMBIGUOUS_MATCH snapshot failures, all in those two runs. Both simulators were booted for 10 run starts (Perf iPhone 17 booted at 12:23Z); 2 of the 10 hit this. Earlier runs likely had only one booted device, so they could not hit the ambiguity.
Repro
Only the second half (wrong error for a missing session) reproduces on demand. In a throwaway state dir, with two iOS simulators booted:
export AGENT_DEVICE_STATE_DIR=$(mktemp -d)
agent-device snapshot --session never-opened
# Error (AMBIGUOUS_MATCH): 2 devices match this request equally; select one explicitly.
# Hint: Select the intended device explicitly, for example --udid <id> or --device "Perf iPhone 17". ...
# (3.6 s)
agent-device daemon stop
The session never-opened does not exist, which is the state a replacement daemon leaves a real session in. Expected: SESSION_NOT_FOUND, as close returns.
To see the whole chain you need a daemon takeover in the middle of a session:
- Boot two iOS simulators, A and B.
agent-device open <app> --session s --platform ios --device A, thenagent-device snapshot --session s(works).- Make the next client call find the daemon unreachable, for example
kill -9 $(jq .pid ~/.agent-device/daemon.json). agent-device snapshot --session s. I expectReplacing daemon … unreachableand thenAMBIGUOUS_MATCH, not tried by hand yet (I did not want to disturb the daemon the other runs share). The e2e runs above are the real occurrence.
Expected and actual
- Expected: a request that names a session the daemon does not hold fails with
SESSION_NOT_FOUND(and says the daemon holds no such session), whatever number of devices is booted. Better: a replaced daemon keeps the session's device, so a configured device is not forgotten. - Actual:
AMBIGUOUS_MATCHafter a 3.3 s device listing. The hint says to pass--udidor--device, which does not describe the problem. The caller learns nothing about the lost session.
Where in the code (v0.21.25)
Why the ambiguity:
src/daemon/snapshot-session.tsL9–L19:resolveSessionDevicedoessession?.device ?? (await resolveTargetDevice(flags ?? {})). A missing session takes the second branch with the request's flags, which carry no selector.snapshotreaches it fromsrc/daemon/snapshot-runtime-binding.tsL153. The same fallback is inwait,alertandsettings.src/daemon/session-device-resolution.tsL34–L49:resolveCommandDevicedoes the same for commands that use it.closehas the right answer already:src/daemon/session-lifecycle/internal/session-close.tsL428.
Why the session is gone:
src/daemon/session-store.tsL51: the live sessions are aMapin the daemon process. Nothing on disk maps a session name to a device, so a new daemon starts empty. (~/.agent-device/sessions/<name>/holds only logs.)
Why the daemon was replaced:
src/daemon-client/daemon-launch-spec.tsL138–L156:resolveDaemonTakeoverreturns{ kind: 'replace', reason: 'unreachable' }whenonClientTransport()is false.src/daemon-client/daemon-client-lifecycle.tsL280–L300:canReachReusableDaemonmakes one probe, then up to three more 200 ms apart, and only while the pid is alive. A dead pid returnsfalseat once. The doc comment above it (L273–L279) already says that replacing a daemon "ends every session the daemon holds".…/daemon-client-lifecycle.tsL193–L220 (readReusableLocalDaemon) prints the notice (L354–L360) and callsretireDaemonForTakeover. The notice carries only the pid, version and the wordunreachable.
main (35c21b6) has the same fallback in packages/daemon/src/core/snapshot-session.ts L9–L19 and the same unreachable path, so this is not fixed upstream yet.
What I could not establish
- Why the daemon was unreachable. Four probes in 0.6 s all failed, or the pid was dead. A single stall in my own client process should not fail all four probes, because the later probes run after the stall ends, so I think the daemon really had no listener, but I cannot prove it. I have no daemon log for the replaced pids:
~/.agent-device/daemon.logappears to hold only the current daemon's output. I found nonodecrash report and no unified-log line for the pid around the time. - Whether another agent-device client shared the state dir. The only other session dir (
cwd_…_default) holds twodoctorevents from 10:46Z. A client that opens no session would leave no trace. - With one simulator booted the same loss presumably shows up as
SESSION_NOT_FOUND/COMMAND_FAILEDrather thanAMBIGUOUS_MATCH. I did not sort the 12 othersnapshot COMMAND_FAILEDevents of the day by cause.
Possible direction
Offered as options, not a design:
- In
resolveSessionDevice(andresolveCommandDevice): when the request names a session and the daemon holds no such session, and the request has no device selector, returnSESSION_NOT_FOUNDwith the session name and the hint toopenagain. That skips the 3 s listing too. Sessionless use (no--session, or an explicit selector) stays as it is. - Say why in the takeover notice and diagnostics: pid alive or dead, which probe failed and how (
ECONNREFUSEDor timeout), how many sessions the replaced daemon held. Keep the olddaemon.log(rotate it) instead of overwriting. - Keep the binding. Either persist
session name -> device id/platformin the state dir so a replacement daemon can re-pin it, or, as #2458 suggests, refuse to replace a daemon whose pid is alive while a session is open and fail with a hint to stop it on purpose.
Related
#1596 (replace then SESSION_NOT_FOUND, closed as expected), #2458 (a replace drops a live session, closed), #3177 (client timeout recovery ends sibling sessions, closed).
Evidence files (event window 13:08:20–13:09:12Z, the two request logs, both run outputs) can be attached on request.
- Lenguaje dominante
- TypeScript
- Estrellas
- 5k
- Forks
- 334
- Merge medio
- 12 h 2 min
- PR fusionados (30 d)
- 678
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de callstack/agent-device
-
`devices` reports a phantom Android emulator named after the emulator's crashdata INFO bannerPosiblemente ocupada @airowe la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
callstack/agent-device#3716 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
callstack/agent-device#1869 ·
Los mantenedores suelen responder en 1 día
-
backlog refactor
Dificultad 5/5 Más de una semana Aptitud para principiantes 8/100
callstack/agent-device#3759 ·
Los mantenedores suelen responder en 1 día
-
backlog enhancement needs-triage
Dificultad 5/5 Más de una semana Aptitud para principiantes 8/100
callstack/agent-device#3758 ·
Los mantenedores suelen responder en 1 día
-
enhancement ready-for-agent
Dificultad 5/5 Más de una semana Aptitud para principiantes 15/100
callstack/agent-device#3757 ·
Los mantenedores suelen responder en 1 día
Todos los issues de callstack/agent-device
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Chocobozzz/PeerTube#7832 ·
Los mantenedores suelen responder en 3 días
-
Pay to use?Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 66/100
darkreader/darkreader#16108 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
Add: Dak Nong TV SDAbiertocheck:passed streams:add
Dificultad 1/5 Menos de una hora Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
bug engine
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
frostney/GocciaScript#1746 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
agilepathway/label-checker#739 · 4 comentarios ·
Los mantenedores suelen responder en 1 día