@tyhld/conductor 0.12.103 → 0.12.105

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.
@@ -11,12 +11,13 @@
11
11
  * 【決め(設計 3ed9e2b2 の 1/4・2/4 と管制の口の決まり 2f5eb92c)】
12
12
  * ・鍵(~/.tyhld/bin/autoupdate/enabled)が無い機では何もしない(★お客さまの機には置かない)。
13
13
  * ・管制に目標の版を聞く。autoUpdate:false・ready:false・止める(halted)・届かない → 何もしない
14
- * (届かないときは更新しない=安全側)。
14
+ * (届かないときは更新しない=安全側)。★ただし【届かなかった回】は、空きを待つ数え(busySince)と
15
+ * 置いた札をそのまま持ち越す(1回の通信の失敗で数えを捨てない・4時間を超えた札だけ外す)。
15
16
  * ・conductor の目標が今と違えば conductor だけを入れ替える。★そのあいだ Claude Code は動かさない
16
17
  * (管制も同じ順番を守る。更新係は管制が返した目標だけを見る)。
17
18
  * ・入れ替えは全現場が空きのときだけ: 札を置く → 中継の1周ぶん待つ → もう一度空きを確かめる →
18
19
  * 版を名指しして入れる(取得先は毎回 https://registry.npmjs.org・@latest にしない)→
19
- * 全現場の常駐を起こし直す → 札を外す → 自己診断 → 管制へ(自己診断が届ける)。
20
+ * 全現場の常駐を起こし直す → 札を外す → 落ち着くのを 45秒 待つ → 自己診断 → 管制へ(自己診断が届ける)。
20
21
  * ・空きでなければ次の回に再挑戦。30分空かなければ札を置いて新しい便を取らない形に移り、
21
22
  * 走っている便の終わりを待つ(★走っている便は止めない)。
22
23
  * ・自己診断が FAIL → そのPCだけ前の版へ戻す → 自己診断(after-rollback=rolledBack が管制へ届く)。
@@ -63,6 +64,29 @@ export const BUSY_LIMIT_MS = 30 * 60 * 1000;
63
64
  export const RESTART_WAIT_MS = 120_000;
64
65
  /** 起こし直した常駐の様子を見る間隔。 */
65
66
  export const RESTART_POLL_MS = 5_000;
67
+ /**
68
+ * 入れ替え(戻し)の直後、自己診断を始めるまで落ち着くのを待つ長さ(★45秒・CC1 決定 2026-10-05・便 36e5680b)。
69
+ *
70
+ * 【なぜ要るか(便 e70843fa で確かめた)】
71
+ * 入れ替えの直後は、全現場の常駐を一度に起こし直してから、間を空けずに自己診断へ進む。
72
+ * 起き直した常駐の子(claude -p)と、自己診断の小さな試験が【同じログインの控えを同時に更新】して重なり、
73
+ * 「認証の更新が別の処理と重なった」で自己診断が落ちやすい。
74
+ * =落ち着くのを少し待ってから測れば、重なり自体が起きにくくなる(根治は待つ側=測る前)。
75
+ * ★当てるのは「入れ替えの直後」と「戻した直後」の自己診断だけ。
76
+ * 毎日の自己診断・人が手で流す自己診断・held の測り直しは1秒も待たない。
77
+ * ★既に入っている「認証の更新の重なりと明確に判定できた失敗だけ、60秒後に1回だけやり直す」
78
+ * (src/selfcheck.ts の SELFCHECK_AUTH_RETRY_WAIT_MS)は1つも変えない。待つのは重なりを【減らす】ため、
79
+ * やり直しは重なってしまった回を【拾う】ため=役目が違うので、両方要る。
80
+ */
81
+ export const SWAP_SETTLE_MS = 45_000;
82
+ /** 落ち着くのを待つ長さを変える環境変数(★秒で書く・0 なら待たない)。 */
83
+ export const SETTLE_ENV = 'CONDUCTOR_AUTOUPDATE_SETTLE_SEC';
84
+ /**
85
+ * 管制に届かないまま、置いた札を外すまでの上限(★4時間・CC1 決定 2026-10-05・便 36e5680b)。
86
+ * 管制が入れ替え待ちの札を🟡にする値(管制 `DRAIN_ALERT_SEC`)と同じ考え=
87
+ * 便を取れないPCのままにしない。★配り役(この側)に同じ意味の定数は無いので、ここに置く。
88
+ */
89
+ export const DRAIN_HOLD_LIMIT_MS = 4 * 60 * 60 * 1000;
66
90
  /** 戻しても直らなかった機で、自己診断を測り直す間隔。 */
67
91
  export const HELD_RECHECK_MS = 60 * 60 * 1000;
68
92
  /**
@@ -213,6 +237,19 @@ function placeDrain(d, s) {
213
237
  s.drainOwned = true;
214
238
  d.log(`入れ替え待ちの札を置きました(新しい便を取りません・走っている便は止めません): ${p}`);
215
239
  }
240
+ /**
241
+ * この係が置いた札を、いつ置いたか(★札のファイルの1行目に書いた時刻を読む=控えに欄を足さない)。
242
+ * 読めない・札が無い → null。
243
+ */
244
+ export function drainPlacedAt(home, env) {
245
+ try {
246
+ const at = Date.parse(readFileSync(drainPath(home, env), 'utf8').trim().split(/\s+/)[0] ?? '');
247
+ return Number.isFinite(at) ? new Date(at) : null;
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ }
216
253
  /** 札を外す。★この係が置いた札だけ(人が置いた札は外さない)。 */
217
254
  function removeDrain(d, s) {
218
255
  if (!s.drainOwned)
@@ -399,10 +436,42 @@ function writePin(d, bin) {
399
436
  renameSync(`${p}.tmp`, p);
400
437
  d.log(`Claude Code の指名を書きました: ${bin}`);
401
438
  }
439
+ /**
440
+ * 落ち着くのを待つ長さ(ミリ秒)。★環境変数(秒)で変えられる・0 なら待たない。
441
+ * 数で読めない・負の数は既定(45秒)=待たない側へ倒さない(安全側)。
442
+ */
443
+ export function settleMs(env) {
444
+ const raw = env[SETTLE_ENV];
445
+ if (raw === undefined || raw.trim() === '')
446
+ return SWAP_SETTLE_MS;
447
+ const sec = Number(raw);
448
+ if (!Number.isFinite(sec) || sec < 0)
449
+ return SWAP_SETTLE_MS;
450
+ return Math.round(sec * 1000);
451
+ }
452
+ /**
453
+ * 入れ替え(戻し)の直後だけ、自己診断を始める前に落ち着くのを待つ。返り値は待った秒数(0=待たない)。
454
+ * ★何秒待ったかを記録に残す(あとから読めるようにする=CC1 決定 A)。
455
+ */
456
+ async function settleBeforeSelfcheck(d, trigger) {
457
+ const ms = settleMs(d.env);
458
+ if (ms <= 0) {
459
+ d.log(`落ち着くのを待たずに自己診断を始めます(${SETTLE_ENV}=0・きっかけ ${trigger})`);
460
+ return 0;
461
+ }
462
+ const sec = Math.round(ms / 1000);
463
+ d.log(`常駐が落ち着くのを ${sec}秒 待ってから自己診断を始めます(認証の更新の重なりを避ける・きっかけ ${trigger})`);
464
+ await d.sleep(ms);
465
+ d.log(`${sec}秒 待ちました。自己診断(${trigger})を始めます`);
466
+ return sec;
467
+ }
402
468
  /** 入れ替え(または戻し)の後の自己診断と、その結果の扱い。 */
403
469
  async function afterSwap(d, s) {
404
470
  const comp = s.component ?? 'conductor';
405
471
  const trigger = s.rollback ? 'after-rollback' : comp === 'conductor' ? 'after-conductor' : 'after-claude';
472
+ // ★起き直した常駐の子と自己診断の小さな試験が、同じログインの控えを同時に更新して重なるのを避ける。
473
+ const waited = await settleBeforeSelfcheck(d, trigger);
474
+ const note = waited > 0 ? `(自己診断の前に ${waited}秒 待ちました)` : '';
406
475
  const r = d.runSelfcheck(trigger);
407
476
  if (r.pass) {
408
477
  if (s.rollback)
@@ -412,7 +481,7 @@ async function afterSwap(d, s) {
412
481
  : `${comp} ${s.to} の自己診断が通りました`;
413
482
  s.phase = 'idle';
414
483
  delete s.rollback;
415
- return done(d, s, s.lastRun?.action === 'held' ? 'recovered' : 'pass', detail);
484
+ return done(d, s, s.lastRun?.action === 'held' ? 'recovered' : 'pass', `${detail}${note}`);
416
485
  }
417
486
  const failed = r.failedKeys.join(',') || '不明';
418
487
  if (!s.rollback) {
@@ -425,12 +494,12 @@ async function afterSwap(d, s) {
425
494
  if (comp === 'claude-code' && back && compareVersions(back, CLAUDE_MIN_VERSION) < 0) {
426
495
  s.phase = 'held';
427
496
  s.heldRecheckAt = new Date(d.now().getTime() + HELD_RECHECK_MS).toISOString();
428
- return done(d, s, 'held', `Claude Code ${s.to} の自己診断が落ちました(${failed})。戻す先 ${back} は最低版 ${CLAUDE_MIN_VERSION} より古いので戻しません(🟡)`);
497
+ return done(d, s, 'held', `Claude Code ${s.to} の自己診断が落ちました(${failed})。戻す先 ${back} は最低版 ${CLAUDE_MIN_VERSION} より古いので戻しません(🟡)${note}`);
429
498
  }
430
499
  if (!back) {
431
500
  s.phase = 'held';
432
501
  s.heldRecheckAt = new Date(d.now().getTime() + HELD_RECHECK_MS).toISOString();
433
- return done(d, s, 'held', `${comp} ${s.to} の自己診断が落ちました(${failed})。戻す先の版が分からないので戻せません(🟡)`);
502
+ return done(d, s, 'held', `${comp} ${s.to} の自己診断が落ちました(${failed})。戻す先の版が分からないので戻せません(🟡)${note}`);
434
503
  }
435
504
  d.log(`${comp} ${s.to} の自己診断が落ちました(${failed})。この機だけ ${back} へ戻します`);
436
505
  return rollBack(d, s, back);
@@ -444,7 +513,7 @@ async function afterSwap(d, s) {
444
513
  placeDrain(d, s);
445
514
  else
446
515
  removeDrain(d, s);
447
- return done(d, s, 'held', `${comp} を ${s.to} へ戻しても自己診断が通りません(${failed})。${cannotWork ? '札を残し、この機は新しい便を受け取りません' : '便の受け取りは続けます'}(🟡)`);
516
+ return done(d, s, 'held', `${comp} を ${s.to} へ戻しても自己診断が通りません(${failed})。${cannotWork ? '札を残し、この機は新しい便を受け取りません' : '便の受け取りは続けます'}(🟡)${note}`);
448
517
  }
449
518
  /** 前の版へ戻す。 */
450
519
  async function rollBack(d, s, back) {
@@ -600,9 +669,21 @@ export async function runUpdater(d) {
600
669
  const { target, detail } = await fetchTarget(d, installed);
601
670
  s.nextCheckAt = new Date(now.getTime() + (target?.checkAgainMin ?? DEFAULT_CHECK_MIN) * 60_000).toISOString();
602
671
  if (!target) {
603
- removeDrain(d, s);
604
- s.busySince = null;
605
- return done(d, s, 'skip', `管制に目標の版を聞けませんでした(${detail})。更新しません`);
672
+ // ★管制に1回届かなかっただけで、空きを待つ数え(busySince)と置いた札を捨てない(便 36e5680b)。
673
+ // 捨てると、便が切れ目なく続くPCでは回線が30分に1回でも途切れるたびに数えが 0 に戻り、
674
+ // 札が置かれないので、いつまでも新しい版へ入れ替わらない(便 443e599d の発見2)。
675
+ // ★更新しないことは今までどおり(届かない回に版は動かさない=安全側)。
676
+ // ★安全のための上限:管制に届かないまま札を置いてから 4時間 を超えたら札を外す
677
+ // (便を取れないPCのままにしない=管制が札を🟡にする値と同じ考え)。
678
+ const placedAt = s.drainOwned ? drainPlacedAt(d.home, d.env) : null;
679
+ const heldMs = placedAt ? now.getTime() - placedAt.getTime() : 0;
680
+ if (placedAt && heldMs >= DRAIN_HOLD_LIMIT_MS) {
681
+ removeDrain(d, s);
682
+ s.busySince = null;
683
+ return done(d, s, 'skip', `管制に目標の版を聞けませんでした(${detail})。札を置いてから ${Math.round(DRAIN_HOLD_LIMIT_MS / 3_600_000)}時間 を超えたので札を外しました(便を取れないPCのままにしない)`);
684
+ }
685
+ const kept = s.drainOwned ? `置いた札はそのまま(${Math.round(heldMs / 60_000)}分)` : '札は置いていません';
686
+ return done(d, s, 'skip', `管制に目標の版を聞けませんでした(${detail})。更新しません(${kept}・空きを待つ数えは持ち越し)`);
606
687
  }
607
688
  if (!target.ready || !target.autoUpdate || target.halted) {
608
689
  removeDrain(d, s);
package/dist/selfcheck.js CHANGED
@@ -21,7 +21,13 @@
21
21
  * 【★落ちたときは理由を残す(便 5a94ba06)】小さな headless 試験が落ちたら、その項目の detail に
22
22
  * 「結果の行の本文・stderr の末尾・終了コード」を残す(控えにも管制にも同じ中身が載る)。
23
23
  * ④囲い・⑤通信の ✗ が「小さな試験が先に落ちたので確かめられなかった」ときは、そう読める文にする。
24
- * ★合否と、落ちたら戻す動きは1つも変えない(FAIL は FAIL・やり直しを足さない)。
24
+ *
25
+ * 【★既知の一時障害1種類だけ、60秒後に1回だけやり直す(えふさん確定 2026-10-05)】
26
+ * 小さな headless 試験が落ちたら理由を分類し、Claude Code の「認証の更新が別の Claude Code の
27
+ * 処理と重なった」と【確かめられた】失敗のときだけ、60秒待って1回だけやり直す。2回目が通れば
28
+ * その自己診断は合格(④囲い・⑤通信も2回目の結果で判定)。再失敗・別種類の失敗は、今までどおり
29
+ * 即 FAIL=戻して配布を止める。★2回目のやり直しは無い。★「2回目が通った=版は正常」とは
30
+ * 一般化しない(この1種類だけの例外)。やり直した事実は ⑥ の detail に残す(控えにも管制にも)。
25
31
  *
26
32
  * 【結果】手元の控え(~/.local/state/conductor/selfcheck/last.json と history/)に必ず残す。
27
33
  * 管制の口が在れば送る(★口が無い・届かないときも落ちない=送れなかったことを控えに書くだけ)。
@@ -645,6 +651,53 @@ export function headlessFailureDetail(h) {
645
651
  out.push(`手がかり: 終了コード ${h.exitCode === null ? '不明' : h.exitCode}`);
646
652
  return out;
647
653
  }
654
+ // ─────────────────────────────────────────────────────────────────────────────
655
+ // 失敗の分類と、1回だけのやり直し(えふさん確定 2026-10-05)
656
+ // ─────────────────────────────────────────────────────────────────────────────
657
+ /**
658
+ * 「認証の更新の重なり」と判定できたときだけ待つ間(★60秒・えふさん確定 2026-10-05)。
659
+ * ★やり直しは60秒後に【1回だけ】(2回目のやり直しは無い)。
660
+ */
661
+ export const SELFCHECK_AUTH_RETRY_WAIT_MS = 60_000;
662
+ /**
663
+ * 「認証の更新の重なり」の文(★実物で確かめた形)。
664
+ *
665
+ * 【実物 2026-10-05】FshimaNP 09:17:28Z(きっかけ after-rollback・0.12.102)と
666
+ * TYHLD-OTA 11:43:43Z(きっかけ after-conductor・0.12.101)の「結果の行の本文」が、どちらも
667
+ * `Failed to refresh OAuth token: another Claude Code process is refreshing it or exited mid-refresh.
668
+ * This is usually transient; retry in a minute, and if it persists close other Claude Code processes
669
+ * or sign in again` だった(終了コード 1・結果の行は subtype=success のまま is_error=true・使用量 0・
670
+ * tool が1つも動かず「AI に届く前に終わった」)。
671
+ * ★2つとも当たることを求める。片方だけ・文が少し違う物は「別種類の失敗」として扱う(やり直さない)。
672
+ */
673
+ const AUTH_REFRESH_CONFLICT_PATTERNS = [
674
+ /failed to refresh oauth token/i,
675
+ /another\b[^.]{0,60}\bprocess is refreshing it\b/i,
676
+ ];
677
+ /**
678
+ * 小さな試験の失敗を分類する(★やり直してよいのは1種類だけ)。
679
+ * 「認証の更新の重なり」と判定するのは、次の【両方】を満たす失敗だけ。
680
+ * (a) 結果の行の本文が、Claude Code の「認証の更新が別の処理と重なった」の文に当たる
681
+ * (b) AI に届く前に終わっている(使用量 0・tool が1つも動いていない)
682
+ * ★迷う物は 'other'(=やり直さない)へ倒す。「2回目が通った=版は正常」とは一般化しない。
683
+ */
684
+ export function classifyHeadlessFailure(h) {
685
+ // 流していない(claude が無い・雛形を読めない)=この種類ではない。
686
+ if (!h.ran)
687
+ return 'other';
688
+ // (a) 結果の行の本文
689
+ const text = h.summary?.result?.text ?? '';
690
+ if (!text.trim())
691
+ return 'other';
692
+ if (!AUTH_REFRESH_CONFLICT_PATTERNS.every((re) => re.test(text)))
693
+ return 'other';
694
+ // (b) AI に届く前に終わっている
695
+ if (reachedModel(h))
696
+ return 'other';
697
+ if ((h.summary?.toolResults.length ?? 0) > 0)
698
+ return 'other';
699
+ return 'auth-refresh-conflict';
700
+ }
648
701
  /** 小さな試験と MCP の確かめで claude に渡す環境(★中継と同じ形・src/headless.ts)。 */
649
702
  function probeEnv(d, claudeBin) {
650
703
  const env = { ...d.env };
@@ -653,15 +706,23 @@ function probeEnv(d, claudeBin) {
653
706
  env.PATH = buildWorkerPath(env.PATH, claudeBin);
654
707
  return env;
655
708
  }
656
- /** ⑥ 小さな headless 試験を1回流す。 */
657
- function runHeadlessProbe(d, claudeBin, runDir) {
709
+ /**
710
+ * 1回目/やり直しの作業場所の名前(★やり直しは別のフォルダで流す)。
711
+ * 同じフォルダで流すと、1回目に外へ書けてしまった跡(probe.txt)が残り、
712
+ * やり直しの「囲いの外へ書けたか」を誤って open と読む。
713
+ */
714
+ export function probeWorkName(attempt) {
715
+ return attempt === 1 ? 'work' : 'work-retry2';
716
+ }
717
+ /** ⑥ 小さな headless 試験を1回流す(attempt=2 はやり直しの回)。 */
718
+ function runHeadlessProbe(d, claudeBin, runDir, attempt = 1) {
658
719
  const res = { summary: null, exitCode: null, outsideWrite: 'unknown', net: {}, probeRan: false, error: null, ran: false, stderr: '' };
659
720
  if (!claudeBin) {
660
721
  res.error = 'claude が無いので流していません';
661
722
  return res;
662
723
  }
663
- const work = path.join(runDir, 'work');
664
- const outside = path.join(runDir, 'outside');
724
+ const work = path.join(runDir, probeWorkName(attempt));
725
+ const outside = path.join(runDir, attempt === 1 ? 'outside' : 'outside-retry2');
665
726
  const outsideFile = path.join(outside, 'probe.txt');
666
727
  mkdirSync(path.join(work, '.claude'), { recursive: true });
667
728
  mkdirSync(outside, { recursive: true });
@@ -699,6 +760,38 @@ function runHeadlessProbe(d, claudeBin, runDir) {
699
760
  res.net[m[1]] = m[2];
700
761
  return res;
701
762
  }
763
+ /**
764
+ * 小さな試験の1回の結果から ⑥ の項目を作る(★1回目もやり直しも【同じ読み方】で判定する)。
765
+ * 合否は職人の言葉でなく、終了コード・結果の行・tool の結果で決める(便 b7bd5e43 から変えない)。
766
+ */
767
+ export function judgeHeadlessProbe(h) {
768
+ const item = { key: 'headless', label: '小さな headless 試験', ok: null, detail: [] };
769
+ const sum = h.summary;
770
+ if (h.error)
771
+ note(item, safeDetail(h.error));
772
+ if (!sum || !sum.result)
773
+ fail(item, 'fault', `結果の行がありません(終了 ${h.exitCode})`);
774
+ else {
775
+ if (h.exitCode === 0)
776
+ pass(item, '終了コード 0');
777
+ else
778
+ fail(item, 'fault', `終了コード ${h.exitCode}`);
779
+ if (sum.result.subtype === 'success' && !sum.result.isError)
780
+ pass(item, '結果の行が success');
781
+ else
782
+ fail(item, 'fault', `結果の行が ${sum.result.subtype ?? '不明'}(is_error=${sum.result.isError})`);
783
+ if (h.probeRan)
784
+ pass(item, '囲いの中で Bash の台本が動きました(tool の結果で確認)');
785
+ else
786
+ fail(item, 'fault', 'tool の結果に台本の目印がありません(Bash が動いていない)');
787
+ }
788
+ // ★落ちたときは「なぜ落ちたか」の手がかりを必ず残す(控えにも管制にも・便 5a94ba06)。
789
+ // 通ったときは1行も足さない(出力は今までと同じ)。
790
+ if (item.ok === false)
791
+ for (const line of headlessFailureDetail(h))
792
+ note(item, line);
793
+ return item;
794
+ }
702
795
  /** +番人:入っている番人に、止めるべき物と通すべき物を1つずつ判定させる。 */
703
796
  function checkGuard(d, work) {
704
797
  const item = { key: 'guard', label: '番人', ok: null, detail: [] };
@@ -856,34 +949,30 @@ export async function runSelfcheck(d, trigger) {
856
949
  const network = net.item;
857
950
  const guard = checkGuard(d, path.join(runDir, 'work'));
858
951
  // ⑥ 小さな試験(③の MCP・④の書き込み・⑤の内側の通信もここで見る)
859
- const headless = { key: 'headless', label: '小さな headless 試験', ok: null, detail: [] };
860
- const h = runHeadlessProbe(d, claude.bin, runDir);
861
- const sum = h.summary;
862
- if (h.error)
863
- note(headless, safeDetail(h.error));
864
- if (!sum || !sum.result)
865
- fail(headless, 'fault', `結果の行がありません(終了 ${h.exitCode})`);
866
- else {
867
- if (h.exitCode === 0)
868
- pass(headless, '終了コード 0');
869
- else
870
- fail(headless, 'fault', `終了コード ${h.exitCode}`);
871
- if (sum.result.subtype === 'success' && !sum.result.isError)
872
- pass(headless, '結果の行が success');
873
- else
874
- fail(headless, 'fault', `結果の行が ${sum.result.subtype ?? '不明'}(is_error=${sum.result.isError})`);
875
- if (h.probeRan)
876
- pass(headless, '囲いの中で Bash の台本が動きました(tool の結果で確認)');
877
- else
878
- fail(headless, 'fault', 'tool の結果に台本の目印がありません(Bash が動いていない)');
952
+ //
953
+ // ★1回目が「認証の更新の重なり」と判定できたときだけ、60秒待って【1回だけ】やり直す
954
+ // (えふさん確定 2026-10-05)。2回目が通ればその自己診断は合格=④囲い・⑤通信も2回目の結果で
955
+ // 判定する。2回目が落ちたら(理由が何であっても)FAIL=今までどおり戻す・配布を止める。
956
+ // ★別種類の失敗は1回も待たずに即 FAIL。きっかけ(daily・after-conductor・after-claude・
957
+ // after-rollback)を問わず同じ動きにする。
958
+ let h = runHeadlessProbe(d, claude.bin, runDir, 1);
959
+ let headless = judgeHeadlessProbe(h);
960
+ let probeAttempt = 1;
961
+ if (headless.ok === false && classifyHeadlessFailure(h) === 'auth-refresh-conflict') {
962
+ const sleep = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
963
+ await sleep(SELFCHECK_AUTH_RETRY_WAIT_MS);
964
+ h = runHeadlessProbe(d, claude.bin, runDir, 2);
965
+ headless = judgeHeadlessProbe(h);
966
+ probeAttempt = 2;
967
+ // ★やり直した事実を残す(控え・管制の両方に・合格した回でも「再試行で通った」と読める)。
968
+ const sec = Math.round(SELFCHECK_AUTH_RETRY_WAIT_MS / 1000);
969
+ note(headless, headless.ok === false
970
+ ? `1回目は認証の更新の重なりで落ちました・${sec}秒後の2回目も落ちました`
971
+ : `1回目は認証の更新の重なりで落ちました・${sec}秒後の2回目で通りました`);
879
972
  }
880
- // ★落ちたときは「なぜ落ちたか」の手がかりを必ず残す(控えにも管制にも・便 5a94ba06)。
881
- // 通ったときは1行も足さない(出力は今までと同じ)。
882
- if (headless.ok === false)
883
- for (const line of headlessFailureDetail(h))
884
- note(headless, line);
973
+ const sum = h.summary;
885
974
  // ③ MCP(★落ち着くまで確かめる・便が実際に使う登録を見る=settleMcp)
886
- const mcp = await settleMcp(d, claude.bin, path.join(runDir, 'work'), sum?.mcp ?? null);
975
+ const mcp = await settleMcp(d, claude.bin, path.join(runDir, probeWorkName(probeAttempt)), sum?.mcp ?? null);
887
976
  const seen = mcp.source === 'mcp-list' ? `claude mcp list で確かめた・${mcp.tries}回目` : mcp.source === 'init' ? '小さな試験の最初の行' : '一覧なし';
888
977
  const mineOf = (st) => (mcp.list ?? []).filter((s) => /ai-kana/i.test(s.name) && s.status === st).map((s) => s.name);
889
978
  if (mcp.state === 'connected') {
@@ -960,7 +1049,8 @@ export async function runSelfcheck(d, trigger) {
960
1049
  rmSync(runDir, { recursive: true, force: true });
961
1050
  // ★道具(Claude Code)が作業場所ごとに /tmp/claude-<uid>/ の下へ作る記録も消す(便 e9276b86)。
962
1051
  // 作業場所は毎回名前が違うので、消さないと自己診断のたびに1つずつ溜まる(ノートPCで9つ)。
963
- for (const dir of [runDir, path.join(runDir, 'work')]) {
1052
+ // ★やり直しの回の作業場所(work-retry2)も同じように消す。
1053
+ for (const dir of [runDir, path.join(runDir, probeWorkName(1)), path.join(runDir, probeWorkName(2))]) {
964
1054
  const trace = claudeTraceDir(dir, d.claudeTmpRoot);
965
1055
  if (trace)
966
1056
  rmSync(trace, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.103",
3
+ "version": "0.12.105",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {