@tyhld/conductor 0.12.110 → 0.12.111

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.
@@ -310,6 +310,60 @@ const ENTRY_SYNC = [
310
310
  // 今までどおり人が設置台本を打つ)。ここで揃えるのは【受け口のファイルだけ】。
311
311
  { from: 'scripts/codex/codex-notify.py', dir: 'hooks', to: 'codex-notify.py', exe: true },
312
312
  ];
313
+ // ── 番人の同期を、版の入れ替えと同じ時に1回起こす(便 b649d1f8・ADR-012 の穴) ──────
314
+ // 【何が起きていたか(実測 2026-10-06)】
315
+ // 受け口(codex-notify.py)は上の ENTRY_SYNC で【入れ替えと同時】に届くのに、
316
+ // 番人(stop_hook.py など)は update-guard.sh の INSTALL_FILES からしか届かず、
317
+ // それを流すのは tyhld-guard-sync.timer=【1時間ごと】。
318
+ // メインPC が 0.12.110 になった 09:57 から、番人がそろう 10:28:45 までの31分間、
319
+ // 「受け口だけ新しく、番人が古い」半死の状態になった。その穴の中で流れた試験
320
+ // e13246e8 は、報告の本文が1文字も管制に載らなかった。
321
+ // =★番人を直した版を公開するたび、各PCで最長1時間その直しが効かない。
322
+ //
323
+ // 【なぜ ENTRY_SYNC に足すのではなく、同期を起こすのか(甲ではなく乙を選んだ理由)】
324
+ // ① 番人は1ファイルで完結しない。stop_hook.py は finish_gate / hook_datalink /
325
+ // summary_ja を import し、finish_gate.py は report-shape.json を実行時に読む。
326
+ // ENTRY_SYNC は1ファイルずつ写すので、ここへ stop_hook.py だけを足すと
327
+ // 「入口だけ新しく、判定の本体と形の決まりが古い」というズレた組み合わせを
328
+ // 新しく作ってしまう(=いま直している穴と同じ種類の事故)。
329
+ // ② それを避けて番人一式(INSTALL_FILES + INSTALL_CONFIGS)を ENTRY_SYNC へ並べると、
330
+ // 配る物の一覧がリポジトリの中で【2か所】になる。今回の穴は「一覧が2つあって
331
+ // 片方に stop_hook.py が無かった」事そのものなので、3つ目を作るのは根治にならない。
332
+ // ③ 同期を1回起こせば、配る中身を決めるのは今までどおり update-guard.sh の
333
+ // INSTALL_FILES だけ(正本は1つ)。番人一式が【同じ版から同時に】置かれるので、
334
+ // ファイルどうしで版がずれる組み合わせが原理的に作れない。
335
+ //
336
+ // 【番人を置いていないPCへ広がらない理由】
337
+ // このユニットを置くのは update-guard.sh の ①-f(APPLY=1 のときだけ)=番人を
338
+ // 設置したPCだけ。つまり【ユニットが在る ⟺ 番人が置いてある】。
339
+ // 下の syncGuardNow は、ユニットの実体が無ければ systemctl を1回も呼ばずに戻る。
340
+ // ★ここから番人を新しく設置する道は1本も無い(置く中身を1バイトも持っていない)。
341
+ /** 番人のコードと配線を正本へ揃える systemd --user サービス(systemd/tyhld-guard-sync.service)。 */
342
+ export const GUARD_SYNC_UNIT = 'tyhld-guard-sync.service';
343
+ /** そのPCに番人の同期サービスが設置されているか(=番人が置いてあるPCか)。 */
344
+ export function guardSyncUnitPath(home) {
345
+ return path.join(home, '.config', 'systemd', 'user', GUARD_SYNC_UNIT);
346
+ }
347
+ /**
348
+ * 番人の同期を1回起こす。★入れ替えた配布物から番人一式が置き直される。
349
+ * 戻り: 記録に残す1行(何もしなかった理由まで分かる形にする)。
350
+ *
351
+ * ★`--no-block` で頼むだけにする理由: 同期の本体(update-guard.sh)は公開 npm への
352
+ * 問い合わせを含み、数十秒かかることがある。入れ替えの手を止めて待つと、常駐の
353
+ * 起こし直しがその分遅れる。頼んだ後は systemd が面倒を見るので、待たなくてよい。
354
+ * ★失敗しても入れ替えは止めない(今までどおり1時間ごとの同期で追いつく)。
355
+ */
356
+ export function syncGuardNow(d) {
357
+ const unit = guardSyncUnitPath(d.home);
358
+ if (!existsSync(unit))
359
+ return '番人の同期: このPCには番人が置かれていないので何もしませんでした';
360
+ const r = d.exec('systemctl', ['--user', 'start', '--no-block', GUARD_SYNC_UNIT], { timeoutMs: 15_000 });
361
+ if (r.status !== 0) {
362
+ return `番人の同期: 起こせませんでした(終了 ${r.status}・${(r.error ?? r.stderr).trim().slice(-120)})`
363
+ + '。1時間ごとの同期で追いつきます';
364
+ }
365
+ return '番人の同期: 起こしました(番人一式が、入れ替えた版から置き直されます)';
366
+ }
313
367
  /** 中身が1バイトも違わないか(★同じなら書き換えない=毎回の入れ替えで日付だけ動かない)。 */
314
368
  function sameContent(a, b) {
315
369
  try {
@@ -529,6 +583,8 @@ async function rollBack(d, s, back) {
529
583
  const r = installConductor(d, back, true);
530
584
  d.log(r.detail);
531
585
  d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
586
+ // ★戻すときも番人を一緒に戻す(入口だけ古い版、番人だけ新しい版を残さない)。
587
+ d.log(syncGuardNow(d));
532
588
  d.log(await restartRelays(d, back));
533
589
  }
534
590
  else {
@@ -785,6 +841,8 @@ export async function runUpdater(d) {
785
841
  return done(d, s, 'install-failed', r.detail);
786
842
  }
787
843
  d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
844
+ // ★番人も【この瞬間】そろえる(1時間ごとの同期を待たない=便 b649d1f8 の穴の根治)。
845
+ d.log(syncGuardNow(d));
788
846
  d.log(await restartRelays(d, want));
789
847
  }
790
848
  else {
package/dist/relay.js CHANGED
@@ -661,7 +661,10 @@ export function relinkClosedCodexNotices(cfg, state, deps = {}, env = process.en
661
661
  turnId: notice.turnId ?? null,
662
662
  });
663
663
  logLine(`site=${cfg.site} id=${job.id} 便を閉じたあとに届いた終わりの知らせを、この便の報告として` +
664
- `結び直しました(印が一致・閉じてから${Math.round((now - job.at) / 1000)}秒): ${got.note}` +
664
+ // ★どの道の知らせで本文が載ったかを記録に出す(便 b649d1f8・決定4。ここは便を閉じる
665
+ // 道ではないが、知らせが報告になる3つ目の場所なので、記録の読み方をそろえる)。
666
+ `結び直しました(印が一致・閉じてから${Math.round((now - job.at) / 1000)}秒` +
667
+ `${codexRouteNote(notice.route)}): ${got.note}` +
665
668
  '/★便の状態は書き換えていません(本文は後から届ける道で管制へ渡します)');
666
669
  }
667
670
  }
@@ -1424,6 +1427,17 @@ export function codexNotifyDir(env = process.env) {
1424
1427
  const home = (env.HOME ?? '').trim();
1425
1428
  return home ? path.join(home, '.tyhld', 'codex-notify') : '';
1426
1429
  }
1430
+ /** 知らせの「道」を記録に出す形にする。分からなければ空(=今までどおりの行になる)。 */
1431
+ export function codexRouteNote(route) {
1432
+ const r = (route ?? '').trim();
1433
+ if (r === '')
1434
+ return '';
1435
+ if (r === 'notify')
1436
+ return '・道=notify(Codex の notify)';
1437
+ if (r === 'stop')
1438
+ return '・道=stop(Codex の Stop フック)';
1439
+ return `・道=${r}`;
1440
+ }
1427
1441
  /**
1428
1442
  * 書きかけの行が末尾に在るとき、書き終わるのを待つ長さ。これを過ぎても改行が来なければ
1429
1443
  * 書き手が落ちた残骸とみなし、今までどおり捨てて進む(=受け皿が詰まらない)。
@@ -1481,6 +1495,7 @@ export function takeCodexNotices(site, env = process.env) {
1481
1495
  turnId: o.turnId ?? null,
1482
1496
  jobId: typeof o.jobId === 'string' && o.jobId.trim() !== '' ? o.jobId.trim() : null,
1483
1497
  at: parseNoticeAt(o.at),
1498
+ route: typeof o.route === 'string' && o.route.trim() !== '' ? o.route.trim() : null,
1484
1499
  });
1485
1500
  }
1486
1501
  }
@@ -1578,6 +1593,7 @@ export function takeCodexNoticesForJobId(site, commandId, env = process.env) {
1578
1593
  turnId: o.turnId ?? null,
1579
1594
  jobId,
1580
1595
  at: parseNoticeAt(o.at),
1596
+ route: typeof o.route === 'string' && o.route.trim() !== '' ? o.route.trim() : null,
1581
1597
  });
1582
1598
  }
1583
1599
  else {
@@ -5100,7 +5116,9 @@ export async function keepaliveTick(cfg, state, deps = {}) {
5100
5116
  if (!ok)
5101
5117
  return; // 書けなければ畳まない(次の周でやり直す。知らせは使い切っている=囲みは報告ファイルに残る)
5102
5118
  logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 追跡が切れたあとに届いた終わりの知らせで完了を報告` +
5103
- `(印が一致${intake.wrote ? '・囲みから報告ファイル' : body !== null ? '・報告添付' : ''})`);
5119
+ `(印が一致${intake.wrote ? '・囲みから報告ファイル' : body !== null ? '・報告添付' : ''}` +
5120
+ // ★どちらの道の知らせで閉じたかを記録に出す(便 b649d1f8・決定4)。
5121
+ `${codexRouteNote(notice.route)})`);
5104
5122
  await deliverJobEndFacts(cfg, {
5105
5123
  commandId: ka.id,
5106
5124
  site: cfg.site,
@@ -5508,7 +5526,9 @@ export async function relayTick(cfg, state, hooks) {
5508
5526
  : await patchStatus(cfg, state.pending.id, 'done', report);
5509
5527
  if (written) {
5510
5528
  log(`site=${cfg.site} id=${state.pending.id} done(Codexの通知` +
5511
- `${intake.wrote ? '・囲みから報告ファイル' : report != null ? '・報告添付' : ''})`);
5529
+ `${intake.wrote ? '・囲みから報告ファイル' : report != null ? '・報告添付' : ''}` +
5530
+ // ★どちらの道の知らせで閉じたかを記録に出す(便 b649d1f8・決定4)。
5531
+ `${codexRouteNote(notice.route)})`);
5512
5532
  // ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。この配り方では実プロセスを
5513
5533
  // 中継が持っていないので、終わりは【つかめていない】と伝える(終了コードは null)。
5514
5534
  // 届けるのは「中継が完了を確かめた」という事実だけ。★作り話の終了を送らない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.110",
3
+ "version": "0.12.111",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -332,13 +332,38 @@ DIST_STATE="$GUARD_HOME/dist-source.state" # 最後に【確かめられた】
332
332
  DIST_LAST="$GUARD_HOME/dist-source.last" # 直近の【試み】の結果
333
333
  DIST_NOTIFY="$SCRIPT_DIR/hooks/skill_sync_notify.py"
334
334
 
335
+ # 入っている版と公開の最新がそろっているか(★控えの1行に残す印)。
336
+ # none … 同じ(入っているのが公開の最新)
337
+ # behind … 違う(段階配布の途中・公開より古い正規の版で動いている)
338
+ # unknown… どちらかが読めない
339
+ #
340
+ # 【なぜ要るか(実測 2026-10-06・便 b649d1f8)】
341
+ # メインPC が 0.12.110 へ入れ替わる31分前の同期は、控えに
342
+ # status=ok reason=verified version=0.12.109 published=0.12.110
343
+ # と書いていた。★この行は「うまくいった」としか読めないが、実際には
344
+ # 「これから入れ替わる版の一つ前の番人を配った」回だった。版の食い違いは
345
+ # 2つの値を自分で見比べないと分からず、見落とすと「配られたつもり」になる。
346
+ # 【決め】status と reason は1文字も変えない(成否は変えない・止めない)。
347
+ # ★食い違いは【別の欄】で残す。status=failed にはしない:公開の最新より古い
348
+ # 正規の版で動くのは段階配布では正常で、2026-10-02 にその理由(version-drift)を
349
+ # 「配らない理由」から外したばかり(便 29babc5d)。そこへ戻さない。
350
+ dist_drift() {
351
+ if [[ -z "${DIST_VERSION:-}" || -z "${DIST_PUBLISHED:-}" ]]; then
352
+ printf 'unknown'
353
+ elif [[ "$DIST_VERSION" == "$DIST_PUBLISHED" ]]; then
354
+ printf 'none'
355
+ else
356
+ printf 'behind'
357
+ fi
358
+ }
359
+
335
360
  # 直近の試みを1行残す。★確かめられたときだけ .state も更新する(成功と偽らない)。
336
361
  # ★確認のみ(APPLY!=1)のときは1バイトも書かない。「見ただけ」で控えが動くと、
337
362
  # 控えが「実際に配った記録」でなくなり、外から見て何が起きたのか分からなくなる。
338
363
  dist_record() {
339
364
  [[ "$APPLY" == "1" ]] || return 0
340
365
  local status="$1" reason="$2" line
341
- line="$(date '+%Y-%m-%dT%H:%M:%S%z') status=${status} reason=${reason} what=dist-source version=${DIST_VERSION:-unknown} published=${DIST_PUBLISHED:-unknown} root=${DIST_ROOT:-none}"
366
+ line="$(date '+%Y-%m-%dT%H:%M:%S%z') status=${status} reason=${reason} what=dist-source version=${DIST_VERSION:-unknown} published=${DIST_PUBLISHED:-unknown} drift=$(dist_drift) root=${DIST_ROOT:-none}"
342
367
  mkdir -p "$GUARD_HOME" 2>/dev/null || true
343
368
  printf '%s\n' "$line" > "$DIST_LAST" 2>/dev/null || true
344
369
  if [[ "$status" == "ok" ]]; then