すべて実コードで裏取り済み。ドキュメントではなくソースを一次情報とした。
| # | 事実 | 根拠(file:line) |
|---|---|---|
| ① | 書き間違い(根本原因): M層の常駐セッションは from bot.process_registry import process_registry を import しようとするが、モジュールが公開する名前は registry だけ。存在しない名前なので ImportError → except で process_registry = None になる。以降 if process_registry and ... は常に偽 → M層のPIDは一度もレジストリに登録されない。 |
persistent_session.py:526-528, 536 process_registry.py:183(公開名は registry) |
| ② | 他の呼び出し元は正しく from bot.process_registry import registry と書けている(=書き間違いはM層登録経路の1箇所だけ)。 |
app.py:40 / claude_runner.py:18 |
| ③ | 代理登録: M層がW層に委譲すると、W層 subprocess は親M層と同じ task_id でレジストリに登録される(_run_workers(delegations, task_id, ...) → run_claude(task_id=task_id, route=d.worker) → registry.register(pid, task_id=task_id))。W層は正常終了時に registry.unregister(pid) で姿を消す。 |
router.py:674, 1158-1164 claude_runner.py:469, 503 |
| ④ | 誤判定エンジン: _registry_writer が5秒ごとに active_task_ids()(=今レジストリに載っているtask_id)を取り、get_orphaned_active_tasks で「EXECUTING/FORMATTING かつ task_idが名簿に無い かつ 更新から60秒超 かつ 未通知 かつ channel有」を孤児と判定 → ERROR化+「中断しました」通知。 |
app.py:3779, 3813-3833 task_tracker.py:288-308(閾値=60秒は :296) |
| ⑤ | 時計が止まる: M層タスクは実行開始時に一度だけ EXECUTING に更新され updated_at が記録される。その後、複数ラウンドの send_prompt・W層委譲が何分続いても updated_at は更新されない(send_prompt は TaskTracker を触らない)。→ 60秒の物差しは正常な長時間処理でも即超える。 |
app.py:1661(EXECUTINGは1回) router.py:635,707,842(以降 update 呼ばず) |
| ⑥ | 物差しの不整合: M層の1回の応答は最大 default_query_timeout=900秒/無出力許容 default_heartbeat_timeout=900秒まで正常。だが孤児判定は60秒で発火。900秒かかる正当な処理を60秒で「中断」と断じうる。 |
persistent_session.py:232, 236 vs task_tracker.py:296 |
| ⑦ | M層の生死は is_alive()(proc is not None and proc.returncode is None)で直接確認できる。だが孤児判定はこれを使っていない(名簿の突合だけ)。 |
persistent_session.py:406-407(未接続) |
| ID | 監視対象 | 実体(コード) | 誰が生死を持つか | 登録状況 |
|---|---|---|---|---|
| T1 | M層セッションプロセス | PersistentSession.proc(常駐 claude CLI)persistent_session.py:381,406 |
proc.returncode | 名簿未登録(バグ①) |
| T2 | W層 worker subprocess | run_claude の subprocess(one-shot)claude_runner.py:456-477 |
proc終了 / watchdog | 親M task_idで登録(代理) |
| T3 | IF層 subprocess | run_claude(route="direct")(直接回答)app.py:1609-1616 |
proc終了 / watchdog | 自身のtask_idで登録 |
| T4 | botプロセス本体 | launchd常駐の event loopapp.py:4187-4206, 4338 | OS / launchd | —(監視する側) |
| T5 | process_registry エントリ | pid → ProcessInfo の辞書process_registry.py:61,64 |
— | register / unregister で増減 |
| T6 | タスク状態(TaskTracker) | JSON永続 Task.status / updated_attask_tracker.py:21-47, 338 |
— | 状態遷移で更新 |
| T7 | レート制限使用率 | get_rate_limit_usage()app.py:3837 |
— | 閾値通知(副次監視) |
| T8 | 3線 Monitor コマンド経路 | orphan_retry / orphan_error / waiting_staleapp.py:3987-4088 |
—(外部投入) | 手動確定の権威 |
判定の凡例: 正常終了 実行中 本物の中断 復旧中
「対象 × 監視契機」ごとに1行。現状の問題行には 誤報 を付す。修正提案は §5。
| # | 監視対象 | いつ監視するか(契機・周期) | どんな監視をするか(何を見るか) | どう判定するか(判定に使うシグナル) | その判定でどう動くか |
|---|---|---|---|---|---|
| M-1 | T1 M層プロセス(孤児判定) | _registry_writer 5秒ループapp.py:3790 |
タスクの task_id が active_task_ids() に有るか+now-updated_at>60s |
現状: 名簿に無い=本物の中断と即断(M本体が未登録なので生存中でも「無い」) | 現状: ERROR化+「中断しました」通知app.py:3824-3831誤報の発火点 |
| M-2 | T1 M層プロセス(直接生死) | send_prompt 入口/restart時persistent_session.py:531 |
is_alive()=proc.returncode is None |
生存でなければ要再起動 | start()で起動し直して継続。※孤児判定には未接続(§5 材料1で接続) |
| M-3 | T1 M層プロセス(無出力) | send_prompt 実行中(heartbeat 900s)persistent_session.py:552 |
直近出力からの経過秒(_read_until_result) |
900s無出力→応答停止(プロセスは生存し得る) | 例外 raise → EXECUTING残留 → M-1へ落ちる(現状は結局誤報/沈黙に収束) |
| M-4 | T1 M層プロセス(絶対timeout) | send_prompt 実行中(query 900s)persistent_session.py:551 |
送信からの総経過秒 | 900s超→本物の中断 | restart後 raise。EXECUTING残留 → M-1へ |
| M-5 | T1 M層プロセス(stdout EOF / stdin broken) | send_prompt 実行中persistent_session.py:530-638 |
stdout EOF / BrokenPipe 例外 | 1回目→自己修復可 / 再発→中断 | restart再送(1回)。失敗で raise → EXECUTING残留 → M-1へ |
| W-1 | T2 W層 subprocess(生死・登録) | 起動時 register / 終了時 unregisterclaude_runner.py:469,503 | subprocess の起動・終了(親M task_idで代理登録) | 正常終了→正常終了(名簿から消える) | 結果をM層へ返す。副作用: 名簿からM task_idが消え、M-1の誤報を誘発誤報の起点 |
| W-2 | T2 W層 subprocess(無活動) | 実行中 _watchdog(heartbeat/absolute)claude_runner.py:494 |
stdout/stderr の last_activity |
無活動→停止(kill対象) | SIGKILL。途中結果を「⚠タイムアウト」注記でM層へ返す |
| W-3 | T2 W層 subprocess(即死/例外) | 起動直後・実行中claude_runner.py:456-500 | returncode即座 / RateLimitError / 例外 | 即死→失敗 | エラー文字列でM層へ返す。全W層失敗ならM起動せず打ち切りrouter.py:693 |
| I-1 | T3 IF層 subprocess | 実行中(register/watchdog/os.kill) | 自身のtask_idで登録・生死監視 | 正常終了→正常終了 / 停止→中断 | 直接回答を返し即COMPLETED。登録が生きているためM層のような誤報は起きにくい(非対称) |
| R-1 | T5 registry エントリ(PID実在) | _registry_writer 5秒ループapp.py:3795-3805 |
os.kill(pid, 0) |
ProcessLookupError→消失 / PermissionError→生存 | 消失なら unregister(pid)。名簿を実態に同期 |
| R-2 | T5 registry エントリ(明示解除) | 送信完了時 finally | W層: registry.unregister(有効)claude_runner.py:503 / M層: process_registry.unregister(None で無効)persistent_session.py:630-633 |
W層→解除成功 / M層→no-op | W層は正しく解除。M層は登録も解除も無効(バグ①の対) |
| R-3 | T5 registry(ダッシュボード) | 5秒ループapp.py:3809 | スナップショットを JSON へ persist | —(可視化) | AGENTS_STATUS_FILE 出力(判定には使わない) |
| K-1 | T6 タスク状態(孤児抽出) | 5秒ループ経由app.py:3814 | get_orphaned_active_tasks(EXECUTING/FORMATTING・未登録・60s超・未通知・channel有) |
該当→本物の中断(現状は誤報混入) | ERROR化+通知(M-1と同一経路) |
| K-2 | T6 タスク状態(起動時掃除) | bot起動時 _cleanup_staletask_tracker.py:113 |
ACTIVEのまま残ったタスク | →STALE(retry_count≥3なら ERROR) | STALE化して再実行の対象にする |
| K-3 | T6 タスク状態(STALE再実行) | 起動時1回(+5分後1回)app.py:3863-3873 | get_unnotified_stale_threads(min_age) |
STALE→復旧中 | 「自動リトライ中」投稿+_retry_thread。稼働中の定期駆動は無し(#545の論点) |
| K-4 | T6 タスク状態(返信待ち) | Monitor waiting_stale TTL |
WAITING_FOR_RESPONSE の滞留 |
プロセス非保持→孤児対象外(正しい) | 孤児判定に入れない。滞留はMonitor TTLに委譲(二重化しない) |
| B-1 | T4 botプロセス(シグナル) | SIGTERM/SIGHUP/SIGINTapp.py:4187-4206 | シグナル受信 | →再起動(in-flight退避なし) | event loop停止。in-flightはK-2で起動後STALE化 |
| B-2 | T7 レート制限 | 5秒ループapp.py:3835-3860 | get_rate_limit_usage() と閾値 |
閾値超→警告 | alert通知(1回/閾値)。判定はタスク中断とは独立 |
| B-3 | T8 Monitor手動確定 | 3線Monitorコマンド投入時app.py:3987-4088 | orphan_retry/orphan_error/waiting_stale |
人/Monitorの意思→復旧/確定中断 | STALE化+リトライ、またはERROR確定(権威的上書き) |
researcher が洗い出した全パターン(W層8・M層11・bot3・registry2=計24)を、上のマトリクスのどの行で捌くかに対応付ける。「現状」列に取りこぼし(正常終了を誤報 or 本物の中断を見逃し)があるものを赤で明示し、「修正後」(§5)で解消することを示す。
| # | パターン | 捌く行 | W層自身の判定 | 現状の取りこぼし(親M層への影響) | 修正後 |
|---|---|---|---|---|---|
| W1 | 正常 exit0 | W-1 / R-2 | 正常 | 親M層に誤報を誘発(最悪ケース) | M本体登録+is_aliveで誤報なし |
| W2 | 非ゼロ exit | W-3 | 失敗を返す | 同上(task_id消失で親誤報リスク) | 解消 |
| W3 | heartbeat kill | W-2 | kill+注記 | 同上 | 解消 |
| W4 | 絶対 timeout | W-2 | kill+注記 | 同上 | 解消 |
| W5 | 即死 | W-3 / R-1 | 失敗を返す | 同上 | 解消 |
| W6 | レート制限 | W-3 | RateLimitError伝播 | 同上 | 解消 |
| W7 | 認証失敗 | W-3 | refresh/失敗を返す | 同上 | 解消 |
| W8 | セッション無効 | W-3 | one-shotで無関係 | 同上 | 解消 |
| # | パターン | 捌く行 | 現状の判定 | 取りこぼし種別 | 修正後 |
|---|---|---|---|---|---|
| M1 | 正常完了 | M-1 | 60s超で誤報しうる | 正常終了を中断誤報 | 誤報なし(材料2+3) |
| M2 | query timeout 900s | M-4→M-1 | 60sで先に誤報→その後ERROR沈黙 | 誤報+沈黙 | is_alive死判定→正しく復旧(材料1) |
| M3 | heartbeat無出力 | M-3→M-1 | 生存中でも中断扱い | 正常/停滞を中断誤報 | is_alive生存→待機(材料1) |
| M4 | stdout EOF | M-5 | restart再送→失敗でM-1 | 再発時ERROR沈黙 | 死判定→STALE復旧(#545後段) |
| M5 | stdin broken | M-5 | restart再送→失敗でM-1 | 再発時ERROR沈黙 | 同上 |
| M6 | レート制限アカウント切替 | M-5相当 | 切替1回で継続 | 切替中の一時未登録 | is_alive生存→待機 |
| M7 | 即死(rc即座) | M-2 | start時RuntimeError | 本物の中断(要復旧) | 死判定→STALE復旧 |
| M8 | 重複プロセスSIGKILL(#662自己killレース) | M-2→M-1 | 復旧プロセス即kill→ループ/沈黙 | 本物の中断(要復旧) | 死判定→STALE復旧。#662修正が前提 |
| M9 | rotate(新session起動) | M-2 | _busyロック中で送信排他 | rotate中の一時未登録 | 生存→待機(材料2で新pid再登録) |
| M10 | account変更 | M-2 | 切替後継続 | 一時未登録 | 生存→待機 |
| M11 | 明示stop(意図的) | M-2 / B-1 | shutdown手続き | —(意図的) | B-1経由で正しく停止 |
| # | パターン | 捌く行 | 現状の判定 | 取りこぼし | 修正後 |
|---|---|---|---|---|---|
| bot1 | シグナル終了 | B-1→K-2 | 再起動後STALE化 | 連鎖再起動時に誤報 | is_alive+バックオフで緩和 |
| bot2 | 起動時 stale 化 | K-2 | ACTIVE→STALE 正常 | — | 維持 |
| bot3 | Monitor手動確定 | B-3 | 権威的確定 正常 | — | 維持 |
| # | パターン | 捌く行 | 現状の判定 | 取りこぼし | 修正後 |
|---|---|---|---|---|---|
| reg1 | os.kill(pid,0) 消失除去 | R-1 | 実態同期 正常 | M層は登録が無く適用されない | M層pidも監視対象になる |
| reg2 | send_prompt finally unregister | R-2 | M層は None で no-op | M層の登録/解除が両方無効 | register/unregisterとも有効化 |
実装はしない。設計方針として3つの材料を「変更ファイル・概要・リスク・#545整合」で整理する。多層防御: 材料2が根治、材料1が安全網、材料3が物差し補正。
| 項目 | 内容 |
|---|---|
| 変更ファイル | bot/persistent_session.py:526 |
| 変更概要 | from bot.process_registry import process_registry → from bot.process_registry import registry as process_registry(app.py:40 と同型に統一)。これでM層のPIDが送信中ずっと名簿に載り、active_task_ids() に task_id が常在する。rotate(M9)で新pidに変わる際は register を撃ち直す点も設計に含める(persistent_session.py:466-469 の start 後)。 |
| リスク | 登録が生き返ると、既存の「名簿にM task_idが有る=孤児でない」判定が初めて意図通り機能する。副作用は「今まで出ていた誤報が止まる」方向のため低リスク。ただし unregister(R-2)も有効化されるので、送信完了〜次状態遷移の数ミリ秒の隙間で瞬間的に未登録になりうる → 材料1/3の物差しで吸収する。 |
| #545整合 | #545 は「孤児と判定された後の復旧」を設計。材料2は「そもそも誤って孤児と判定しない」検知側の根治で、#545の前提。誤検知を残したまま#545の攻めた自動リトライを有効化すると健全タスクを再実行してしまうため、材料2は#545 Phase1より先行または同時が必須。 |
| 項目 | 内容 |
|---|---|
| 変更ファイル | bot/app.py(_registry_writer の孤児処理 3817-3831)/M層セッション参照ヘルパ |
| 変更概要 | 孤児候補と判定した後、ERROR化+通知の直前に task.route から該当M層 PersistentSession を引き、is_alive()(persistent_session.py:406)で生死を確認する。生存=実行中→何もしない(通知しない)/死亡=本物の中断→STALE化して#545の復旧へ。名簿突合(間接シグナル)に、生死の直接確認を重ねる二段判定。 |
| リスク | M層セッションはグローバル管理(route→session)を参照する必要がある。参照先が無いrouteは従来通り名簿判定にフォールバック。is_alive は proc の returncode を見るだけで副作用なし・低コスト。「無出力だが生存」(M3)を中断と誤らないのが最大の利得。 |
| #545整合 | #545 の「孤児→STALE統一」の入口に、is_aliveゲートを1枚噛ませる形。#545の復旧経路はそのまま活かし、入口の判定精度だけ上げる。競合しない上積み。 |
| 項目 | 内容 |
|---|---|
| 変更ファイル | bot/task_tracker.py:296(閾値)/bot/persistent_session.py:send_prompt(心拍で updated_at 更新) |
| 変更概要 | (a) send_prompt のストリーム受信中に、TaskTracker の updated_at を定期的に叩いて「時計を動かす」(現状は EXECUTING 設定時の1回だけで凍結)。(b) 孤児閾値60秒を、default_query_timeout=900s/heartbeat=900s と矛盾しない値へ引き上げる(例: heartbeatと同等 or 名簿突合が生きるので短くても可)。材料2でM本体が登録されれば閾値超過だけでは孤児にならないため、(b)は保険。 |
| リスク | updated_at更新の頻度が高すぎるとJSON書き込みが増える → ストリーム全行ではなく「Nラウンドごと/M秒ごと」に間引く。閾値引き上げは「本物の中断の検出が遅れる」トレードオフだが、材料1のis_aliveが即時に死を捉えるため遅延は実質吸収される。 |
| #545整合 | #545 は孤児由来STALEに min_age の短縮(60〜120s)を提案。材料3の閾値はこれと意味が逆(検知を緩め、誤報を減らす)なので、「検知=材料1/2/3で正確化」→「復旧=#545のmin_ageで機敏化」と役割分担を明文化して衝突を避ける。 |
| 観点 | #545(orphan recovery) | 本設計 #552(orphan monitoring) | 関係 |
|---|---|---|---|
| 担当領域 | 孤児と判定された後の復旧(何をするか) | 孤児かどうかの検知精度(誤判定しないか) | 相補(検知→復旧) |
| 核心の変更 | 孤児→STALE統一+稼働中の定期リカバリ駆動 | import修正(材料2)+is_aliveゲート(材料1)+物差し補正(材料3) | 本設計は#545の前提 |
| import バグ① | 言及なし | 根本原因として特定・修正提案 | 本設計で新規 |
| is_alive の活用 | 言及なし | 誤報防止の安全網として接続 | 本設計で新規 |
| 閾値/min_age | 孤児由来STALEの min_age を短縮(機敏な復旧) | 孤児閾値を実作業時間へ整合(誤報削減) | 役割分担で衝突回避(検知は正確に/復旧は機敏に) |
| #662依存 | Phase1の前提(自己killレース) | M8の正しい復旧に同じく#662修正が前提 | 一致 |
| 導入順序 | Phase0観測→Phase1配線 | 材料2→1→3 を#545 Phase1より先行 | 本設計を先に |
_do_start 自己killレース)の修正が、M8を「本物の中断→復旧」で正しく回すための前提(#545と共通)。persistent_session.py / app.py / task_tracker.py)に閉じる。M層/W層/ベンダー経路の外部I/Fは不変。process_registry.count)にM層が現れるようになる(表示が実態に近づく・望ましい変化)。| 案 | 内容 | 採否 | 理由 |
|---|---|---|---|
| 案A | 材料2+1+3 の多層防御(根治+安全網+物差し) | 採用 | — |
| 案B | 閾値を60s→900sに上げるだけ | 却下 | 本物の中断の検出が900s遅れる。M本体未登録の根本は残る |
| 案C | 孤児検出自体を無効化 | 却下 | 誤報は消えるが本物の中断を全て見逃す(要件違反) |
| 案D | watchdog監視スレッド再導入で能動監視 | 却下 | #655/#660削除方針に逆行・#662レース増幅(#545と同判断) |