monster-orchestrator 3.7.6 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-obf/.buildinfo +5 -5
- package/dist-obf/account-failover-tick.js +1 -1
- package/dist-obf/governor-tick.js +1 -1
- package/dist-obf/index.js +1 -1
- package/dist-obf/manifest.json +1 -1
- package/dist-obf/package.json +4 -4
- package/dist-obf/supervisor-notify-tick.js +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/monster-deathcheck +165 -13
|
@@ -40,7 +40,8 @@ exit code:
|
|
|
40
40
|
MONSTER_ROOT monster レジストリの場所(既定 ~/.claude/monsters)
|
|
41
41
|
TMUX_BIN tmux の絶対パス(既定は PATH → /opt/homebrew/bin/tmux の順に探索)
|
|
42
42
|
⚠️ 非対話 SSH では PATH に /opt/homebrew/bin が乗らないため必要
|
|
43
|
-
CLAUDE_PROJECTS_ROOT transcript の場所(既定 ~/.claude/projects
|
|
43
|
+
CLAUDE_PROJECTS_ROOT transcript の場所(既定 ~/.claude/projects)。足跡が無い席(移行前)だけ読む
|
|
44
|
+
BLUELAMP_FOOTPRINTS_ROOT 足跡の場所(既定 ~/.bluelamp/footprints・箱 n=2475 §足跡の形 v1)
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
47
|
import glob
|
|
@@ -247,11 +248,11 @@ def find_ghosts(monsters, tmux):
|
|
|
247
248
|
# 🚨 GHOST と STALLED は同時に成立しうる(実測: 23b47504)。
|
|
248
249
|
# 片方が他方を隠さないよう、ghost 側にも API 状態を載せる。
|
|
249
250
|
gid = next((k for k in monsters if k.startswith(short)), None) or short
|
|
250
|
-
readable,
|
|
251
|
+
readable, stalled, _cause = read_api_verdict(gid)
|
|
251
252
|
out.append({
|
|
252
253
|
"session": sess,
|
|
253
254
|
"short_id": short,
|
|
254
|
-
"stalled":
|
|
255
|
+
"stalled": stalled if readable else None,
|
|
255
256
|
"stalled_unverified": not readable,
|
|
256
257
|
"spawned_at": spawned.get(sess, ""),
|
|
257
258
|
"detail": ("tmux にセッションが在るが state.json にエントリが無い。"
|
|
@@ -330,6 +331,109 @@ def read_last_api_state(monster_id):
|
|
|
330
331
|
return (False, None, None)
|
|
331
332
|
|
|
332
333
|
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# 足跡(箱 n=2475 §足跡の形 v1)— 席は会話の記録を書かない(CLAUDE_CODE_SKIP_PROMPT_HISTORY=1)。
|
|
336
|
+
# 起動子のフックが本文なしの 1 行を ~/.bluelamp/footprints/<cwd のスラグ>/<sid>.jsonl に足す:
|
|
337
|
+
# {"v":1,"ts":…,"sid":…,"cwd":…,"ev":"api_error","err":"rate_limit"} 等
|
|
338
|
+
# transcript 版と同じ問い(最後の応答は API エラーか)を、本文を持たない足跡で答える:
|
|
339
|
+
# 最後の ev=api_error の後に、席が応答した印(FOOTPRINT_RESPONDED)が 1 つも無ければ止まっている。
|
|
340
|
+
# 🔑 prompt は応答の印に数えない(transcript 版も user 行の後に assistant 行が来るまで最後のエラーを見続ける)。
|
|
341
|
+
# ⚠️ 足跡が在る席は足跡だけを読む。足跡が無い席(移行前)だけ transcript を読む。
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
FOOTPRINTS_ROOT = os.environ.get("BLUELAMP_FOOTPRINTS_ROOT") or os.path.expanduser("~/.bluelamp/footprints")
|
|
345
|
+
|
|
346
|
+
FOOTPRINT_TAIL_LINES = 200
|
|
347
|
+
|
|
348
|
+
# api_error の後に来たら「席が応答した」印になる足跡の種別
|
|
349
|
+
FOOTPRINT_RESPONDED = {"tool", "tool_fail", "stop", "session_start"}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def footprint_dir(monster_id):
|
|
353
|
+
"""monster の足跡のディレクトリ。無ければ None(=移行前の席)。"""
|
|
354
|
+
hits = [d for d in glob.glob(os.path.join(FOOTPRINTS_ROOT, f"*monsters-{monster_id}*")) if os.path.isdir(d)]
|
|
355
|
+
return hits[0] if hits else None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def last_footprint_error(lines):
|
|
359
|
+
"""足跡の行列から、止めている api_error の err を返す純関数(--verify で単体検証する)。
|
|
360
|
+
|
|
361
|
+
返り値: None=止まっていない/文字列=最後の api_error の err(その後に応答の印が無い)
|
|
362
|
+
v が 1 でない行・壊れた行は読まない。
|
|
363
|
+
"""
|
|
364
|
+
last_err = None
|
|
365
|
+
for line in lines:
|
|
366
|
+
line = line.strip()
|
|
367
|
+
if not line:
|
|
368
|
+
continue
|
|
369
|
+
try:
|
|
370
|
+
e = json.loads(line)
|
|
371
|
+
except json.JSONDecodeError:
|
|
372
|
+
continue
|
|
373
|
+
if not isinstance(e, dict) or e.get("v") != 1:
|
|
374
|
+
continue
|
|
375
|
+
ev = e.get("ev")
|
|
376
|
+
if ev == "api_error":
|
|
377
|
+
err = e.get("err")
|
|
378
|
+
last_err = err if isinstance(err, str) and err else "unknown"
|
|
379
|
+
elif ev in FOOTPRINT_RESPONDED:
|
|
380
|
+
last_err = None
|
|
381
|
+
return last_err
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def read_last_footprint_state(monster_id):
|
|
385
|
+
"""足跡から API 状態を返す。
|
|
386
|
+
|
|
387
|
+
返り値: None=足跡が無い(ディレクトリが無い/1 本も無い=移行前の席・transcript へ。
|
|
388
|
+
orchestrator の footprint.ts readSeatFootprintTail と同じ線引き)/(readable, err)
|
|
389
|
+
readable=False は「足跡のファイルは在るが読めない」。⚠️ 正常に丸めない。
|
|
390
|
+
"""
|
|
391
|
+
d = footprint_dir(monster_id)
|
|
392
|
+
if d is None:
|
|
393
|
+
return None
|
|
394
|
+
files = glob.glob(os.path.join(d, "*.jsonl"))
|
|
395
|
+
if not files:
|
|
396
|
+
return None
|
|
397
|
+
try:
|
|
398
|
+
with open(max(files, key=os.path.getmtime), errors="replace") as f:
|
|
399
|
+
lines = f.readlines()[-FOOTPRINT_TAIL_LINES:]
|
|
400
|
+
except OSError:
|
|
401
|
+
return (False, None)
|
|
402
|
+
return (True, last_footprint_error(lines))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def classify_footprint_error(err):
|
|
406
|
+
"""足跡の err から判定名を返す純関数(--verify で単体検証する)。
|
|
407
|
+
|
|
408
|
+
transcript 版(classify_api_error)と同じ 3 つに畳む:
|
|
409
|
+
rate_limit(transcript では 429)→ STALLED-RATE-LIMIT
|
|
410
|
+
server_error・overloaded(transcript では 5xx・529 Overloaded)→ STALLED-BACKEND
|
|
411
|
+
(err の語は StopFailure の error=箱 n=2475 の W1 実測・Claude Code 2.1.289 の列挙)
|
|
412
|
+
それ以外(authentication_failed 等・transcript では status なし)→ STALLED-API-ERROR
|
|
413
|
+
"""
|
|
414
|
+
if not err:
|
|
415
|
+
return None
|
|
416
|
+
if err == "rate_limit":
|
|
417
|
+
return "STALLED-RATE-LIMIT"
|
|
418
|
+
if err in ("server_error", "overloaded"):
|
|
419
|
+
return "STALLED-BACKEND"
|
|
420
|
+
return "STALLED-API-ERROR"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def read_api_verdict(monster_id):
|
|
424
|
+
"""席の API 応答不能の判定。足跡が在れば足跡だけ、無ければ transcript(移行の間)。
|
|
425
|
+
|
|
426
|
+
返り値 (readable, verdict, cause)
|
|
427
|
+
verdict は STALLED-* か None。cause は詳細表示用("HTTP 429" / "err=rate_limit")。
|
|
428
|
+
"""
|
|
429
|
+
fp = read_last_footprint_state(monster_id)
|
|
430
|
+
if fp is not None:
|
|
431
|
+
readable, err = fp
|
|
432
|
+
return (readable, classify_footprint_error(err) if readable else None, f"err={err}")
|
|
433
|
+
readable, is_err, status = read_last_api_state(monster_id)
|
|
434
|
+
return (readable, classify_api_error(is_err, status) if readable else None, f"HTTP {status}")
|
|
435
|
+
|
|
436
|
+
|
|
333
437
|
def classify_api_error(is_api_error, status):
|
|
334
438
|
"""API エラー状態から判定名を返す純関数(--verify で単体検証する)。
|
|
335
439
|
|
|
@@ -345,15 +449,16 @@ def classify_api_error(is_api_error, status):
|
|
|
345
449
|
return "STALLED-API-ERROR"
|
|
346
450
|
|
|
347
451
|
|
|
348
|
-
def stalled_detail(verdict,
|
|
452
|
+
def stalled_detail(verdict, cause):
|
|
453
|
+
"""cause は詳細表示用の原因(transcript なら "HTTP 503"・足跡なら "err=server_error")。"""
|
|
349
454
|
if verdict == "STALLED-RATE-LIMIT":
|
|
350
|
-
return ("API が上限で応答しない(
|
|
455
|
+
return (f"API が上限で応答しない({cause})。state.json は running・tmux も在席・"
|
|
351
456
|
"session.log も更新され続けるが、次の 1 文字も出力できない。"
|
|
352
457
|
"盤面の稼働数が嘘になるので、配車先として数えないこと")
|
|
353
458
|
if verdict == "STALLED-BACKEND":
|
|
354
|
-
return (f"API がバックエンド障害で応答しない(
|
|
355
|
-
"
|
|
356
|
-
return f"API が応答しない(
|
|
459
|
+
return (f"API がバックエンド障害で応答しない({cause})。"
|
|
460
|
+
"上限と違い、次の瞬間に回復しうる")
|
|
461
|
+
return f"API が応答しない({cause})"
|
|
357
462
|
|
|
358
463
|
def tail_since_last_prompt(events):
|
|
359
464
|
"""最後の user_prompt / spawned 以降のイベント列を切り出す"""
|
|
@@ -383,15 +488,14 @@ def classify(mid, rec, events_by_id, tmux):
|
|
|
383
488
|
"state.json は running だが tmux セッションが3回とも取れない")
|
|
384
489
|
|
|
385
490
|
# tmux に居ても API が応答しないことがある。
|
|
386
|
-
readable,
|
|
491
|
+
readable, stalled, cause = read_api_verdict(mid)
|
|
387
492
|
if not readable:
|
|
388
493
|
# ⚠️ 読めなかったものを ALIVE に丸めない(正常と未確認は別物)
|
|
389
494
|
return ("ALIVE-UNVERIFIED",
|
|
390
495
|
"tmux セッション在席を実測(3回確認)/"
|
|
391
|
-
"⚠️ transcript
|
|
392
|
-
stalled = classify_api_error(is_err, status)
|
|
496
|
+
"⚠️ 足跡も transcript も読めず、API に応答できるかは未確認")
|
|
393
497
|
if stalled:
|
|
394
|
-
return (stalled, stalled_detail(stalled,
|
|
498
|
+
return (stalled, stalled_detail(stalled, cause))
|
|
395
499
|
|
|
396
500
|
return ("ALIVE", "tmux セッション在席を実測(3回確認)")
|
|
397
501
|
|
|
@@ -555,6 +659,52 @@ def verify_stalled():
|
|
|
555
659
|
return ok
|
|
556
660
|
|
|
557
661
|
|
|
662
|
+
def verify_footprint():
|
|
663
|
+
"""足跡の判定の単体検証と、付け替え前後(transcript/足跡)が同じ判定を返すかの表(箱 n=2475)。
|
|
664
|
+
|
|
665
|
+
🚨 偽陽性の方が危険(働いている席を STALLED と誤ると「止めてよい」に見える)。
|
|
666
|
+
"""
|
|
667
|
+
def fp(ev, **kw):
|
|
668
|
+
return json.dumps({"v": 1, "ts": "2026-10-04T08:00:00Z", "sid": "s", "cwd": "/x", "ev": ev, **kw})
|
|
669
|
+
|
|
670
|
+
# (場面, transcript の (is_api_error, status), 足跡の行列)
|
|
671
|
+
scenes = [
|
|
672
|
+
("rate_limit(上限)", (True, 429), [fp("prompt"), fp("tool", tool="Bash", ok=True), fp("api_error", err="rate_limit")]),
|
|
673
|
+
("authentication_failed(失効)", (True, None), [fp("prompt"), fp("api_error", err="authentication_failed")]),
|
|
674
|
+
("server_error(バックエンド)", (True, 500), [fp("prompt"), fp("api_error", err="server_error")]),
|
|
675
|
+
("overloaded(バックエンド)", (True, 529), [fp("prompt"), fp("api_error", err="overloaded")]),
|
|
676
|
+
("billing_error(その他)", (True, 400), [fp("prompt"), fp("api_error", err="billing_error")]),
|
|
677
|
+
("正常", (False, None), [fp("prompt"), fp("tool", tool="Bash", ok=True), fp("stop")]),
|
|
678
|
+
("上限の後に回復", (False, None), [fp("api_error", err="rate_limit"), fp("prompt"), fp("tool", tool="Bash", ok=True)]),
|
|
679
|
+
("上限の後に続きを投げたが未応答", (True, 429), [fp("api_error", err="rate_limit"), fp("prompt")]),
|
|
680
|
+
("配下の席の終わり・pane の終わりは応答でない", (True, 429), [fp("api_error", err="rate_limit"), fp("subagent_stop"), fp("session_end")]),
|
|
681
|
+
]
|
|
682
|
+
ok = True
|
|
683
|
+
seen = set()
|
|
684
|
+
for label, (is_err, status), lines in scenes:
|
|
685
|
+
before = classify_api_error(is_err, status)
|
|
686
|
+
after = classify_footprint_error(last_footprint_error(lines))
|
|
687
|
+
seen.add(after)
|
|
688
|
+
good = before == after
|
|
689
|
+
ok = ok and good
|
|
690
|
+
print(f" {'OK ' if good else 'NG '}{label:<28} transcript={before} 足跡={after}")
|
|
691
|
+
# 壊れた行・v が違う行は読まない
|
|
692
|
+
for label, lines, want in [
|
|
693
|
+
("壊れた行", ["{壊れた", ""], None),
|
|
694
|
+
("v が 1 でない", [json.dumps({"v": 2, "ev": "api_error", "err": "rate_limit"})], None),
|
|
695
|
+
("空", [], None),
|
|
696
|
+
]:
|
|
697
|
+
got = classify_footprint_error(last_footprint_error(lines))
|
|
698
|
+
good = got == want
|
|
699
|
+
ok = ok and good
|
|
700
|
+
print(f" {'OK ' if good else 'NG '}{label:<28} → {got}")
|
|
701
|
+
# 🔬 陽性対照: 表の判定が全部 None に潰れていない
|
|
702
|
+
good = {"STALLED-RATE-LIMIT", "STALLED-BACKEND", "STALLED-API-ERROR", None} <= seen
|
|
703
|
+
ok = ok and good
|
|
704
|
+
print(f" {'OK ' if good else 'NG '}陽性対照: 場面で判定が変わる → {sorted(str(x) for x in seen)}")
|
|
705
|
+
return ok
|
|
706
|
+
|
|
707
|
+
|
|
558
708
|
def verify():
|
|
559
709
|
"""対照実験: 測定器が既知ケースを正しく分類できるか。
|
|
560
710
|
|
|
@@ -565,6 +715,8 @@ def verify():
|
|
|
565
715
|
ghost_ok = verify_ghost()
|
|
566
716
|
print("\n--- API 応答不能の判定(純粋関数・実データ非依存)---")
|
|
567
717
|
stalled_ok = verify_stalled()
|
|
718
|
+
print("\n--- 足跡の判定と付け替え前後の一致(純粋関数・実データ非依存・箱 n=2475)---")
|
|
719
|
+
stalled_ok = verify_footprint() and stalled_ok
|
|
568
720
|
print(f"\n--- 死亡判定(既知ケース・実データ依存)---")
|
|
569
721
|
cases = {
|
|
570
722
|
# short_id: 期待する分類(人手で一次資料まで確認済み・2026-08-04)
|
|
@@ -607,7 +759,7 @@ def ghost_as_row(g):
|
|
|
607
759
|
if g.get("stalled"):
|
|
608
760
|
detail += f"/+ {g['stalled']}: API が応答しない(2 つの死角に同時該当)"
|
|
609
761
|
elif g.get("stalled_unverified"):
|
|
610
|
-
detail += "/+ transcript
|
|
762
|
+
detail += "/+ 足跡も transcript も読めず、応答可能かは未確認"
|
|
611
763
|
return {
|
|
612
764
|
"short_id": g["short_id"],
|
|
613
765
|
"spawned_at": g.get("spawned_at") or "(spawn 記録なし)",
|