@tyhld/conductor 0.12.104 → 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.
Files changed (2) hide show
  1. package/dist/autoupdate.js +90 -9
  2. package/package.json +1 -1
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.104",
3
+ "version": "0.12.105",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {