Daemon startup: wait for a live daemon instead of a fixed 15 s deadline on slow or overloaded hosts
メンテナーはふだん 1 日以内に返信
関連するプルリクエストがすでにマージされています。
- #3791 @thymikee による — マージ済み
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- node.js, typescript
調査の方向性
Start with startLocalDaemon and waitForDaemonStartup in src/daemon-client/daemon-client-lifecycle.ts; trace how startup results, timeouts, and daemon logs are handled. Then read the existing lifecycle and startup-race tests and the daemon environment-variable documentation. Done when the completion conditions pass, including typed failure reasons, configurable timing, progress reporting, and updated help or docs.
索引モデルが issue の本文から書いたものです。
説明
Purpose
On a slow or overloaded host, the CLI kills a healthy daemon that is still starting. The result is daemon_startup_failed, and the run fails before any work starts. This happens on any CI that can clog: shared macOS runners, small Linux containers, Windows runners, and nested virtualization. It also happens on a developer machine under load. A slow host must make startup slower, not fail it.
#3342 records this failure class (more than 12 instances on unrelated diffs since 2026-10-08; re-runs at the same head pass). This issue is the fix contract for that class.
Current behavior (origin/main)
startLocalDaemon in src/daemon-client/daemon-client-lifecycle.ts:
- It sets one wall-clock deadline:
DAEMON_STARTUP_TIMEOUT_MS = 15_000(line 83, used at line 480). waitForDaemonStartup(line 818) polls until the daemon publishes a reachabledaemon.json, or until the process exits, or until the deadline.- At the deadline, the client retires the daemon, even when the process is alive. A
timeoutresult never retries (retryis true only forearly_exit), soDAEMON_STARTUP_ATTEMPTS = 2does not help. Every report showsstartupAttempts: 1. - No setting can change the 15 s budget.
Evidence from run 39019020372 (PR #3760, 2026-10-17):
- The step
pnpm clean:daemontook 70 s on that runner, which shows that the host was overloaded. - The CLI spawned daemon pid 6849 at 09:30:03. The client killed it 15 s later (
cleanupResults: retired / forced / exited). daemon.logwas empty (0 bytes), so the daemon did not reach its first log line. The process was alive but did not get enough CPU.- The same signature occurred on
refactor/host-environment-root(run 39008970059) andfix/provider-not-registered(run 38993655494), which do not contain that PR's commits.
Required behavior
-
Wait for a live daemon. While the spawned daemon process is alive and has not exited, the client continues to wait for registration. A slow start is not a failure.
-
Fail fast on a real failure. If the daemon process exits before it registers, the client reports the failure immediately, as it does now (
early_exit,unproven). -
Keep a hard ceiling. A daemon that stays alive but never registers must not hang the client. The client stops at a ceiling. The default ceiling must be large enough for an overloaded CI host (proposal: 120 s). Measure the slowest successful startup in CI logs before you choose the number.
-
Show progress. The daemon writes startup phase markers (for example
spawned,modules-loaded,lock-acquired,listening,registered) with timestamps, to the place the client already reads (daemon.logtail or the startup metadata). The client can then tell "slow but progressing" from "stuck". If a marker shows progress, the client keeps waiting up to the ceiling. If no marker appears for a long time, the client can stop earlier than the ceiling. Put the stall window next to the ceiling, as one constant. -
Make the budget configurable. An environment variable sets the ceiling, for example
AGENT_DEVICE_DAEMON_STARTUP_TIMEOUT_MS. It applies to the CLI, MCP, and the Node client, because all three usestartLocalDaemon(viaensureDaemon(), #3765 and #3766). Document it with the other daemon environment variables. Do not add CI auto-detection. Rule 1 makes the default safe on every host. -
Use typed failure reasons.
details.kindstaysdaemon_startup_failed. A newdetails.reasontells the caller what happened:startup-exited: the process exited before it registered.startup-stalled: the process is alive, but no progress marker appeared within the stall window.startup-ceiling: the process made progress, but did not register before the ceiling.
Each reason has its own hint. For
startup-stalledandstartup-ceiling, the hint says that the host can be overloaded, and names the environment variable. Callers branch ondetails.reason, never on the message. -
Record timings. The failure details include the time of each phase marker that the client saw, and
startupTimeoutMsshows the effective ceiling.
Example failure payload:
{
"code": "COMMAND_FAILED",
"message": "Failed to start daemon",
"details": {
"kind": "daemon_startup_failed",
"reason": "startup-stalled",
"startupTimeoutMs": 120000,
"phases": { "spawned": 0, "modules-loaded": 41250 },
"hint": "The daemon is alive but made no startup progress for 60 s. The host can be overloaded. Retry, or raise AGENT_DEVICE_DAEMON_STARTUP_TIMEOUT_MS."
}
}
Completion conditions
- A test starts a daemon that is alive but slow (for example a test-only startup delay of 20 s, longer than the old 15 s budget). Startup succeeds, and the client does not kill the daemon.
- A test starts a daemon that exits during startup. The client reports
startup-exitedwithin 1 s after the exit. - A test starts a daemon that stays alive and writes no phase marker. The client reports
startup-stalledat the stall window, not at the ceiling. - A test sets the environment variable to a small value and sees
startup-ceilingwith the newstartupTimeoutMs. - The existing lifecycle and startup-race tests pass without change in meaning.
- The
daemon-clienthelp or user docs name the environment variable and the three reasons. - After the change merges, #3342 gets a
Current state:comment. Then the iOS Smoke preflight step is observed for one week. The issue closes if nodaemon_startup_failedwith reasonstartup-stalledorstartup-ceilingappears below the new ceiling.
Out of scope
- Re-running CI lanes to hide the failure.
- The slow
pnpm clean:daemonstep in.github/workflows/ios.yml(70 s on an overloaded runner). That is a separate CI cost. Report it in #3342 if it repeats.
Dependencies
- #3342: failure-class log and evidence.
- #3765 and #3766 (merged):
ensureDaemon()and attach mode. They callstartLocalDaemon, so the change applies to all clients.
- 主要言語
- TypeScript
- スター
- 5k
- フォーク
- 334
- 平均マージ
- 12時間 13分
- マージ済み PR(30日)
- 723
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
callstack/agent-device のほかの issue
-
bug ready-for-agent
難易度 2/5 1〜3時間 初心者へのやさしさ 87/100
callstack/agent-device#3819 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
callstack/agent-device#1869 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 76/100
callstack/agent-device#3851 ·
メンテナーはふだん 1 日以内に返信
-
feat(hardware-button): press phone volume and power buttons on Android and iOS対応中かも @thymikee が今日担当しました。 オープンneeds-triage
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
callstack/agent-device#3848 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
callstack/agent-device#3844 ·
メンテナーはふだん 1 日以内に返信
callstack/agent-device の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
logancyang/obsidian-copilot#3753 ·
メンテナーはふだん 1 日以内に返信
-
area: presentation severity: important
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
voithos-labs/aragonite#763 ·
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
diegosouzapw/OmniRoute#17386 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 半日 初心者へのやさしさ 72/100
super-productivity/super-productivity#10982 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信