A session lost to a daemon takeover ("Replacing daemon … unreachable") answers the next request with AMBIGUOUS_MATCH when two simulators are booted
维护者通常 1 天内回复
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 25/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- ios, nodejs, typescript
调研方向
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.
由索引模型根据 Issue 内容生成。
描述
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.
- 主要语言
- TypeScript
- 星标
- 5k
- 派生
- 334
- 平均合并
- 12 小时 2 分钟
- 30 天内合并 PR
- 678
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
callstack/agent-device 的其他 Issue
-
`devices` reports a phantom Android emulator named after the emulator's crashdata INFO banner可能已有人在做 @airowe 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 75/100
callstack/agent-device#3716 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
callstack/agent-device#1869 ·
维护者通常 1 天内回复
-
backlog refactor
难度 5/5 一周以上 新手友好度 8/100
callstack/agent-device#3759 ·
维护者通常 1 天内回复
-
backlog enhancement needs-triage
难度 5/5 一周以上 新手友好度 8/100
callstack/agent-device#3758 ·
维护者通常 1 天内回复
-
enhancement ready-for-agent
难度 5/5 一周以上 新手友好度 15/100
callstack/agent-device#3757 ·
维护者通常 1 天内回复
查看 callstack/agent-device 的全部 Issue
相似的 Issue
-
Endpoint links in the tag operations list are below the 24px target size可能已有人在做 @AdzerKI 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
ai-driven-qa bug Minor
难度 2/5 1-3 小时 新手友好度 80/100
linagora/twake-tasks#435 ·
维护者通常 1 天内回复
-
bug domain: database priority: p1 Stellar Wave
难度 2/5 1-3 小时 新手友好度 74/100
StellarState/SS-backend#632 · 1 条评论 ·
维护者通常 1 天内回复
-
A2A audit timestamps end in +00:00Z (invalid ISO 8601)可能已有人在做 @O1sumitkumar 今天认领。 未关闭bug good first issue help wanted python
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
`cf complete zsh >> .zshrc` fails on vanilla zsh可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 2/5 1-3 小时 新手友好度 75/100
cloudflare/cf#340 ·
维护者通常 1 天内回复