@tyhld/conductor 0.12.100 → 0.12.102
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/autoupdate.js +104 -16
- package/dist/headless.js +10 -4
- package/dist/relay.js +37 -14
- package/dist/selfcheck.js +28 -3
- package/package.json +1 -1
- package/scripts/hooks/skill_sync_notify.py +22 -10
- package/scripts/hooks/test_skill_sync_notify.py +42 -17
package/dist/autoupdate.js
CHANGED
|
@@ -252,33 +252,82 @@ function installConductor(d, version, offline) {
|
|
|
252
252
|
return { ok: false, detail: `npm install の終了 ${r.status}・入っている版 ${now ?? '不明'}(${(r.error ?? r.stderr).trim().slice(-200)})` };
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
255
|
+
* 入れ替え後に揃える物(配布物の中の元 → 置き場 → 実行権を付けるか)。
|
|
256
|
+
* ★`dir` は ~/.tyhld の下の置き場の名前。番人が職人の書き込みを止める場所なので、
|
|
257
|
+
* ここを書けるのは更新係・設置台本(人が打つ物)だけ。
|
|
258
|
+
*/
|
|
259
|
+
const ENTRY_SYNC = [
|
|
260
|
+
{ from: 'scripts/tyhld-autoupdate.sh', dir: 'bin', to: 'tyhld-autoupdate', exe: true },
|
|
261
|
+
{ from: 'scripts/tyhld-selfcheck.sh', dir: 'bin', to: 'tyhld-selfcheck', exe: true },
|
|
262
|
+
{ from: 'scripts/dist-source.sh', dir: 'bin', to: 'dist-source.sh', exe: false },
|
|
263
|
+
{ from: 'scripts/conductor-run.sh', dir: 'bin', to: 'conductor-run', exe: true },
|
|
264
|
+
{ from: 'scripts/hooks/skill_sync_notify.py', dir: 'bin', to: 'skill_sync_notify.py', exe: false },
|
|
265
|
+
// ★Codex の完了の受け口(便 08af46e0)。
|
|
266
|
+
// 【なぜここで揃えるか】受け口の置き場(~/.tyhld/hooks/codex-notify.py)は
|
|
267
|
+
// ~/.codex/config.toml の notify が【絶対パス】で指す1か所。版が上がって中身が変わっても、
|
|
268
|
+
// 各PCで人が設置台本(install-codex-hooks.sh)を打つまで古い受け口が残り、
|
|
269
|
+
// 新しい配り役が読む受け皿に完了の知らせが入らない。
|
|
270
|
+
// 【広げないための決め】★既に受け口が置いてあるPCだけに写す(下の `not-installed`)。
|
|
271
|
+
// 未設置のPC(Codex を使っていないPC)には1バイトも置かない。
|
|
272
|
+
// ★~/.codex/hooks.json と ~/.codex/config.toml には触らない(設定を変える道は、
|
|
273
|
+
// 今までどおり人が設置台本を打つ)。ここで揃えるのは【受け口のファイルだけ】。
|
|
274
|
+
{ from: 'scripts/codex/codex-notify.py', dir: 'hooks', to: 'codex-notify.py', exe: true },
|
|
275
|
+
];
|
|
276
|
+
/** 中身が1バイトも違わないか(★同じなら書き換えない=毎回の入れ替えで日付だけ動かない)。 */
|
|
277
|
+
function sameContent(a, b) {
|
|
278
|
+
try {
|
|
279
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 更新係・自己診断・常駐の入口と Codex の完了の受け口を、入れ替えた配布物から写し直す
|
|
287
|
+
* (★在る物だけ・原子的に置き換える)。
|
|
256
288
|
* 常駐の入口(conductor-run・dist-source.sh・skill_sync_notify.py)は毎時同期でも揃うが、
|
|
257
289
|
* 起こし直す前に揃えておく(新しい配布物の探し方で起きる)。
|
|
258
290
|
*/
|
|
259
291
|
export function refreshEntries(home, distRoot) {
|
|
260
|
-
const bin = path.join(home, '.tyhld', 'bin');
|
|
261
|
-
const pairs = [
|
|
262
|
-
['scripts/tyhld-autoupdate.sh', 'tyhld-autoupdate', true],
|
|
263
|
-
['scripts/tyhld-selfcheck.sh', 'tyhld-selfcheck', true],
|
|
264
|
-
['scripts/dist-source.sh', 'dist-source.sh', false],
|
|
265
|
-
['scripts/conductor-run.sh', 'conductor-run', true],
|
|
266
|
-
['scripts/hooks/skill_sync_notify.py', 'skill_sync_notify.py', false],
|
|
267
|
-
];
|
|
268
292
|
const out = [];
|
|
269
|
-
for (const
|
|
293
|
+
for (const { from, dir, to, exe } of ENTRY_SYNC) {
|
|
270
294
|
const src = path.join(distRoot, from);
|
|
271
|
-
const dst = path.join(
|
|
272
|
-
if (!existsSync(src)
|
|
295
|
+
const dst = path.join(home, '.tyhld', dir, to);
|
|
296
|
+
if (!existsSync(src)) {
|
|
297
|
+
out.push({ name: to, outcome: 'no-source' });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// ★既に置いてある物だけを揃える(未設置のPCには新しく置かない)。
|
|
301
|
+
if (!existsSync(dst)) {
|
|
302
|
+
out.push({ name: to, outcome: 'not-installed' });
|
|
273
303
|
continue;
|
|
304
|
+
}
|
|
305
|
+
if (sameContent(src, dst)) {
|
|
306
|
+
out.push({ name: to, outcome: 'same' });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
274
309
|
copyFileSync(src, `${dst}.tmp`);
|
|
275
310
|
if (exe)
|
|
276
311
|
chmodSync(`${dst}.tmp`, 0o755);
|
|
277
312
|
renameSync(`${dst}.tmp`, dst);
|
|
278
|
-
out.push(to);
|
|
313
|
+
out.push({ name: to, outcome: 'synced' });
|
|
279
314
|
}
|
|
280
315
|
return out;
|
|
281
316
|
}
|
|
317
|
+
/** 揃えた結果を1行の日本語にする(★記録に残して、あとから何をしたか読めるようにする)。 */
|
|
318
|
+
export function describeEntrySync(results) {
|
|
319
|
+
const label = {
|
|
320
|
+
synced: '揃えた',
|
|
321
|
+
same: '揃える必要が無かった',
|
|
322
|
+
'not-installed': '置いていないので何もしなかった',
|
|
323
|
+
'no-source': '配布物に元が無かった',
|
|
324
|
+
};
|
|
325
|
+
const parts = ['synced', 'same', 'not-installed', 'no-source']
|
|
326
|
+
.map((k) => ({ k, names: results.filter((r) => r.outcome === k).map((r) => r.name) }))
|
|
327
|
+
.filter((g) => g.names.length > 0)
|
|
328
|
+
.map((g) => `${label[g.k]}=${g.names.join(',')}`);
|
|
329
|
+
return `入口と受け口の同期: ${parts.length > 0 ? parts.join('/') : '対象なし'}`;
|
|
330
|
+
}
|
|
282
331
|
/** 常駐(conductor@*・耳・司令塔)の一覧。 */
|
|
283
332
|
function relayUnits(d) {
|
|
284
333
|
const r = d.exec('systemctl', ['--user', 'list-units', 'conductor@*.service', 'conductor-ear.service', 'conductor-build-runner.service', '--all', '--no-legend', '--plain'], { timeoutMs: 15_000 });
|
|
@@ -410,7 +459,7 @@ async function rollBack(d, s, back) {
|
|
|
410
459
|
if (comp === 'conductor') {
|
|
411
460
|
const r = installConductor(d, back, true);
|
|
412
461
|
d.log(r.detail);
|
|
413
|
-
refreshEntries(d.home, d.distRoot);
|
|
462
|
+
d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
|
|
414
463
|
d.log(await restartRelays(d, back));
|
|
415
464
|
}
|
|
416
465
|
else {
|
|
@@ -471,6 +520,40 @@ function waitingForHuman(d, now) {
|
|
|
471
520
|
detail: `自己診断を測り直しましたが、まだ通りません(${r.failedKeys.join(',') || '不明'}${r.humanOnly ? '・人の操作待ち' : ''})`,
|
|
472
521
|
};
|
|
473
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* 最後の自己診断が「仕組みの異常(🟡・fault)」で落ちたままなら、HELD_RECHECK_MS ごとに測り直す(便 cbc27db6)。
|
|
525
|
+
*
|
|
526
|
+
* 【なぜ要るか(実物 2026-10-05 00:19Z・メインPC)】
|
|
527
|
+
* スリープから起きた直後の毎日の自己診断で、「管制との通信」の1行だけが時間切れで落ちた
|
|
528
|
+
* (ほかの項目は全部通っていた)=回線が戻りきる前の、その場かぎりの失敗。
|
|
529
|
+
* ところがこの FAIL は【次の毎日の自己診断(翌日)まで残る】。そのあいだ管制は🟡のままで、
|
|
530
|
+
* この機は後続の順番から外れ、新しい版を受け取れない。人が手で流し直すまで戻らなかった。
|
|
531
|
+
* =人の手で直すのではなく、更新係が測り直して、通ったら自分で元に戻れるようにする(根治)。
|
|
532
|
+
*
|
|
533
|
+
* 【緩めていない所】
|
|
534
|
+
* ★合否の決め方は1つも変えない。通らなければ今までどおり FAIL のまま(管制が🟡にし、順番から外す)。
|
|
535
|
+
* ★入れ替えの直後(after-conductor・after-claude)の FAIL には当てない。
|
|
536
|
+
* あちらは phase が swapped / held で先に処理されるので、ここまで来ない(戻す → 自己診断のまま)。
|
|
537
|
+
* ★測り直すたびに小さな試験の費用がかかるので、間隔は held の測り直しと同じ(それより短くしない)。
|
|
538
|
+
* ★常駐がまだ起きていない起動時(boot)は測らない(常駐が動いていない=落ちて当たり前)。
|
|
539
|
+
* ★通ったことは自己診断自身が管制へ届ける(更新係は届け直さない)。
|
|
540
|
+
*/
|
|
541
|
+
function recheckAfterFault(d, now) {
|
|
542
|
+
if (d.boot)
|
|
543
|
+
return '';
|
|
544
|
+
const last = readLastSelfcheck(d.home);
|
|
545
|
+
if (!last || last.pass || last.humanOnly)
|
|
546
|
+
return '';
|
|
547
|
+
const age = last.finishedAt === null ? Infinity : now.getTime() - last.finishedAt;
|
|
548
|
+
if (age < HELD_RECHECK_MS)
|
|
549
|
+
return '';
|
|
550
|
+
const r = d.runSelfcheck('daily');
|
|
551
|
+
const detail = r.pass
|
|
552
|
+
? '仕組みの異常(🟡)で落ちていた自己診断を測り直したら通りました(管制へも届きました)'
|
|
553
|
+
: `仕組みの異常(🟡)の自己診断を測り直しましたが、まだ通りません(${r.failedKeys.join(',') || '不明'})`;
|
|
554
|
+
d.log(detail);
|
|
555
|
+
return detail;
|
|
556
|
+
}
|
|
474
557
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
475
558
|
// 1回ぶん
|
|
476
559
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -530,6 +613,10 @@ export async function runUpdater(d) {
|
|
|
530
613
|
// ③-2 人の操作待ち(🔵)で自己診断が落ちたままなら、測り直してから先へ進む(便 4cf71dde)。
|
|
531
614
|
// ★落ちたまま入れ替えると、同じ理由で落ちて戻し、版に疑いが付く(実物 2026-10-03 02:43Z・ノートPC)。
|
|
532
615
|
const human = waitingForHuman(d, now);
|
|
616
|
+
// ③-3 仕組みの異常(🟡)で自己診断が落ちたままなら、held と同じ間隔で測り直す(便 cbc27db6)。
|
|
617
|
+
// ★その場かぎりの時間切れで落ちた FAIL が翌日まで残り、この機が順番から外れたままになるのを直す。
|
|
618
|
+
// ★入れ替えるかどうかの決め方は1つも変えない(通らなければ今までどおり FAIL のまま)。
|
|
619
|
+
const refault = recheckAfterFault(d, now);
|
|
533
620
|
// ④ どの部品を動かすか(★conductor の目標が今と違うあいだは Claude Code を動かさない)。
|
|
534
621
|
const claudeNow = d.resolveClaude();
|
|
535
622
|
let comp = null;
|
|
@@ -565,7 +652,8 @@ export async function runUpdater(d) {
|
|
|
565
652
|
// ★後続の機は候補を探さない・候補の自己診断を流さない(役は管制が決める・欄が無ければ今までどおり)。
|
|
566
653
|
if (target.role !== 'follower')
|
|
567
654
|
await reportCandidates(d, s, installed, claudeNow?.version ?? null);
|
|
568
|
-
|
|
655
|
+
const same = `目標の版と同じです(conductor ${installed ?? '不明'}・Claude Code ${claudeNow?.version ?? '不明'})`;
|
|
656
|
+
return done(d, s, 'none', refault ? `${same}。${refault}` : same);
|
|
569
657
|
}
|
|
570
658
|
if (human.waiting) {
|
|
571
659
|
removeDrain(d, s);
|
|
@@ -615,7 +703,7 @@ export async function runUpdater(d) {
|
|
|
615
703
|
removeDrain(d, s);
|
|
616
704
|
return done(d, s, 'install-failed', r.detail);
|
|
617
705
|
}
|
|
618
|
-
refreshEntries(d.home, d.distRoot);
|
|
706
|
+
d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
|
|
619
707
|
d.log(await restartRelays(d, want));
|
|
620
708
|
}
|
|
621
709
|
else {
|
package/dist/headless.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createInterface } from 'node:readline';
|
|
|
19
19
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
|
-
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, ackResumeSignal, buildAnchor, buildSendText, canDeliver,
|
|
22
|
+
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, ackResumeSignal, buildAnchor, buildSendText, canDeliver, completeWithReportOutcome, createRelayState, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, WAIT_CLOSED_STATUS, FINISH_GATE_KINDS, reportDelivery, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
23
23
|
import { claudePinPath, readClaudePinLine, } from './autoupdate-flags.js';
|
|
24
24
|
const log = (msg) => {
|
|
25
25
|
console.log(`[headless] ${msg}`);
|
|
@@ -1471,10 +1471,16 @@ async function checkRunningJob(cfg, state) {
|
|
|
1471
1471
|
// ★書けるまでやり直す(便 6ebba0d0)。以前は false のとき patchStatus を1回試し、
|
|
1472
1472
|
// 成否を見ずに state.job を畳んでいた=誰も done を書いていないのに便を手放していた。
|
|
1473
1473
|
// 畳まない間は次の便を配らない/管制には生存報告と理由が残る。
|
|
1474
|
-
|
|
1474
|
+
// ★「どう畳んだか」を見て、手番の記録にも実際を書く(便 f20dd761 の候補3)。
|
|
1475
|
+
// 以前は畳めたら必ず outcome=done・detail=「報告まで出しました」だった。
|
|
1476
|
+
// 本番操作の承認待ちで閉じた手番(完了は1バイトも書いていない)まで
|
|
1477
|
+
// 「報告まで出しました」になり、`kc 現場名` が「終わりました」と見せていた。
|
|
1478
|
+
// ★札(outcome)は増やさない。実際を言うのは detail の役目にする。
|
|
1479
|
+
const settle = await completeWithReportOutcome(cfg, job.commandId, log);
|
|
1480
|
+
if (settle.settled) {
|
|
1475
1481
|
state.doneWriteTries?.delete(job.commandId);
|
|
1476
|
-
log(`site=${cfg.site} id=${job.commandId}
|
|
1477
|
-
noteLastJob('done',
|
|
1482
|
+
log(`site=${cfg.site} id=${job.commandId} 畳みます(exit=0, attempt ${job.attempt}・${settle.detail})`);
|
|
1483
|
+
noteLastJob('done', settle.detail);
|
|
1478
1484
|
state.job = null;
|
|
1479
1485
|
rs.lastHeartbeatAt = null;
|
|
1480
1486
|
return;
|
package/dist/relay.js
CHANGED
|
@@ -3491,6 +3491,8 @@ export async function fetchControl(cfg) {
|
|
|
3491
3491
|
return {
|
|
3492
3492
|
relayHalted: true,
|
|
3493
3493
|
ok: false,
|
|
3494
|
+
// ★例外で落ちた=時間切れ・回線の失敗。断られた答え(HTTP 非2xx)はここへ来ない(上で返している)。
|
|
3495
|
+
transient: true,
|
|
3494
3496
|
detail: err instanceof Error ? err.message : String(err),
|
|
3495
3497
|
};
|
|
3496
3498
|
}
|
|
@@ -3679,8 +3681,16 @@ export async function patchStatus(cfg, id, status, report) {
|
|
|
3679
3681
|
* 書けなければ未処理のまま残す=次tickでやり直せるし、管制も生存報告で気づける。
|
|
3680
3682
|
*
|
|
3681
3683
|
* 返り: 追跡を畳んでよいか(true = 便の終了状態が確定した/false = 書けていない=次の周でやり直す)。
|
|
3684
|
+
* ★★「どう畳んだか」も要るときは completeWithReportOutcome を呼ぶ(手番の記録に実際を書くため)。
|
|
3682
3685
|
*/
|
|
3683
3686
|
export async function completeWithReport(cfg, id, logLine = log) {
|
|
3687
|
+
return (await completeWithReportOutcome(cfg, id, logLine)).settled;
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* 完了を1回で送り、【どう畳んだか】も返す(★中身の説明は上の completeWithReport と同じ)。
|
|
3691
|
+
* 呼び手は settled だけ見てもよいが、手番の記録を書くときは kind・detail も使う。
|
|
3692
|
+
*/
|
|
3693
|
+
export async function completeWithReportOutcome(cfg, id, logLine = log) {
|
|
3684
3694
|
// ★①報告がどの段にあるか+形が整っているか(送る工程の話)。②③の実行可否には使わない。
|
|
3685
3695
|
// ★形が整っていない報告は「届いた」と数えない(便 461be339)。ファイルは消さず・移さず残す。
|
|
3686
3696
|
const delivery = reportDelivery(id);
|
|
@@ -3697,7 +3707,7 @@ export async function completeWithReport(cfg, id, logLine = log) {
|
|
|
3697
3707
|
(phase === 'pending' && !delivery.delivered
|
|
3698
3708
|
? `→relayからは送りません(${delivery.note}=未処理のまま残します)`
|
|
3699
3709
|
: '→relayからは送りません(報告は控えとして処理済みにしました)'));
|
|
3700
|
-
return true;
|
|
3710
|
+
return { settled: true, kind: 'already-finished', detail: `管制で既に終わっていました(${already})` };
|
|
3701
3711
|
}
|
|
3702
3712
|
if (isWaitClosedCommandStatus(already)) {
|
|
3703
3713
|
// ★待ちのまま閉じられた便(本番操作の承認待ち・管制 PR #425)。★完了を1バイトも書かない。
|
|
@@ -3709,30 +3719,43 @@ export async function completeWithReport(cfg, id, logLine = log) {
|
|
|
3709
3719
|
logLine(`site=${cfg.site} id=${id} は待ちのまま閉じられています(status=${already})` +
|
|
3710
3720
|
'→完了は書きません(本番操作が済めば管制の合図で同じ便の続きから起きます)' +
|
|
3711
3721
|
(phase === 'none' ? '' : `/報告は手元の控えに残します(${phase})`));
|
|
3712
|
-
return
|
|
3722
|
+
return {
|
|
3723
|
+
settled: true,
|
|
3724
|
+
kind: 'wait-closed',
|
|
3725
|
+
// ★ここが「報告まで出しました」になっていると、承認を待っている手番が
|
|
3726
|
+
// 「終わりました」と記録される(便 f20dd761 の候補3)。実際を書く。
|
|
3727
|
+
detail: '本番操作の承認待ちのまま閉じました(完了は書いていません。承認が済めば続きから起きます)',
|
|
3728
|
+
};
|
|
3713
3729
|
}
|
|
3714
3730
|
if (phase === 'processed') {
|
|
3715
3731
|
// ★①は済み・②は未了。本文は渡してあるので、状態だけを書く(開発履歴を2件にしない)。
|
|
3716
3732
|
// 書けなければ false=次の周でやり直す(完了を落とさない)。
|
|
3717
|
-
if (!(await patchStatus(cfg, id, 'done')))
|
|
3718
|
-
return false;
|
|
3733
|
+
if (!(await patchStatus(cfg, id, 'done'))) {
|
|
3734
|
+
return { settled: false, kind: 'not-written', detail: '便の終了状態を管制へ書けませんでした' };
|
|
3735
|
+
}
|
|
3719
3736
|
logLine(`site=${cfg.site} id=${id} done(報告は回収の道で渡し済み=本文は添えない)`);
|
|
3720
|
-
return true;
|
|
3737
|
+
return { settled: true, kind: 'done', detail: '報告まで出しました' };
|
|
3721
3738
|
}
|
|
3722
3739
|
// ★形が整っていない報告は添えない(書きかけを完了の材料にしない)。便の確定は今までどおり行う
|
|
3723
3740
|
// =現場を占有したままにしない。「報告が届いていない」ことは終了の事実で管制へ届く。
|
|
3724
3741
|
const report = delivery.delivered && delivery.raw !== null ? parseReport(delivery.raw).body : null;
|
|
3725
|
-
if (!(await patchStatus(cfg, id, 'done', report)))
|
|
3726
|
-
return false;
|
|
3742
|
+
if (!(await patchStatus(cfg, id, 'done', report))) {
|
|
3743
|
+
return { settled: false, kind: 'not-written', detail: '便の終了状態を管制へ書けませんでした' };
|
|
3744
|
+
}
|
|
3727
3745
|
if (report !== null)
|
|
3728
3746
|
markReportProcessed(id); // ★消さずに処理済みへ(同じ報告を二度送らない)
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
return
|
|
3747
|
+
const how = report !== null
|
|
3748
|
+
? `(報告添付・${report.length}文字)`
|
|
3749
|
+
: phase === 'none'
|
|
3750
|
+
? '(報告ファイルなし=完了のみ)'
|
|
3751
|
+
: `(${delivery.note}=添えずに未処理のまま残します)`;
|
|
3752
|
+
logLine(`site=${cfg.site} id=${id} done${how}`);
|
|
3753
|
+
return {
|
|
3754
|
+
settled: true,
|
|
3755
|
+
kind: 'done',
|
|
3756
|
+
// ★報告が無い/形が整っていない回を「報告まで出しました」と言わない。
|
|
3757
|
+
detail: report !== null ? '報告まで出しました' : `完了は書きました${how}`,
|
|
3758
|
+
};
|
|
3736
3759
|
}
|
|
3737
3760
|
/**
|
|
3738
3761
|
* 「実プロセスが無い・報告は渡し済み・管制ではまだ終わっていない」便に、便の終了状態だけを書く
|
package/dist/selfcheck.js
CHANGED
|
@@ -235,6 +235,12 @@ export function aiKanaMcpState(mcp) {
|
|
|
235
235
|
export const MCP_SETTLE_TRIES = 3;
|
|
236
236
|
export const MCP_SETTLE_WAIT_MS = 5_000;
|
|
237
237
|
export const MCP_LIST_TIMEOUT_MS = 45_000;
|
|
238
|
+
/**
|
|
239
|
+
* 「緊急停止の読み取り」が時間切れ・回線の失敗で通らなかったとき、読み直すまでの待ち(便 cbc27db6)。
|
|
240
|
+
* ★読み直しは1回だけ(回線が戻るのを待つだけなので、何度も読まない)。
|
|
241
|
+
* ★管制が断った答え(HTTP 401・403・500 など)は読み直さない=この待ちも入らない。
|
|
242
|
+
*/
|
|
243
|
+
export const CONTROL_RETRY_WAIT_MS = 5_000;
|
|
238
244
|
/**
|
|
239
245
|
* `claude mcp list` の出力を読む(★実機 2026-10-03・Claude Code 2.1.287 の形)。
|
|
240
246
|
* `claude.ai ai-kana: https://… - ✔ Connected` / `ai-kana: https://… (HTTP) - ! Needs authentication` /
|
|
@@ -459,11 +465,30 @@ async function checkCentral(d) {
|
|
|
459
465
|
return item;
|
|
460
466
|
}
|
|
461
467
|
const site = d.env.CONDUCTOR_SITE || 'selfcheck';
|
|
462
|
-
const
|
|
468
|
+
const read = () => withFetch(d, () => fetchControl({ url, token, machine: d.machine, site }));
|
|
469
|
+
let ctl = await read();
|
|
470
|
+
/** 1回目が「その場かぎりの失敗」で通らなかったときの、その理由(読み直していなければ null)。 */
|
|
471
|
+
let firstDetail = null;
|
|
472
|
+
if (!ctl.ok && ctl.transient) {
|
|
473
|
+
// ★時間切れ・回線の失敗のときだけ、少し待って1回だけ読み直す(便 cbc27db6)。
|
|
474
|
+
// 実物 2026-10-05 00:19Z メインPC:スリープから起きた直後の毎日の自己診断で、回線が戻りきる前に
|
|
475
|
+
// 読んで時間切れ(The operation was aborted due to timeout)。ほかは全部通っているのに FAIL になり、
|
|
476
|
+
// 🟡 が次の日の自己診断まで残って、この機が後続の順番から外れた。
|
|
477
|
+
// ★管制が断った答え(HTTP 401・403・500 など)と応答が不正は読み直さない(待っても変わらない)。
|
|
478
|
+
firstDetail = ctl.detail;
|
|
479
|
+
const sleep = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
480
|
+
await sleep(CONTROL_RETRY_WAIT_MS);
|
|
481
|
+
ctl = await read();
|
|
482
|
+
}
|
|
483
|
+
const again = firstDetail === null
|
|
484
|
+
? ''
|
|
485
|
+
: ctl.ok
|
|
486
|
+
? `・1回目は通りませんでした(${firstDetail})・2回目で通りました`
|
|
487
|
+
: `・1回目も通りませんでした(${firstDetail})`;
|
|
463
488
|
if (ctl.ok)
|
|
464
|
-
pass(item, `緊急停止の読み取りが通りました(停止中=${ctl.relayHalted ? 'はい' : 'いいえ'}
|
|
489
|
+
pass(item, `緊急停止の読み取りが通りました(停止中=${ctl.relayHalted ? 'はい' : 'いいえ'})${again}`);
|
|
465
490
|
else
|
|
466
|
-
fail(item, 'fault', `緊急停止の読み取りが通りません(${ctl.detail}
|
|
491
|
+
fail(item, 'fault', `緊急停止の読み取りが通りません(${ctl.detail})${again}`);
|
|
467
492
|
return item;
|
|
468
493
|
}
|
|
469
494
|
/** relay.ts の fetch を試験で差し替えるための包み(globalThis.fetch を一時的に置き換える)。 */
|
package/package.json
CHANGED
|
@@ -110,22 +110,32 @@ REINSTALL = ('npm install -g @tyhld/conductor '
|
|
|
110
110
|
RESYNC = 'APPLY=1 bash "$(npm root -g)/@tyhld/conductor/scripts/update-guard.sh"'
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def autoupdate_enabled():
|
|
114
|
-
"""自動更新が入っている PC か(自社PCの鍵 ~/.tyhld/bin/autoupdate/enabled・便 29babc5d)。
|
|
115
|
-
|
|
113
|
+
def autoupdate_enabled(home=None):
|
|
114
|
+
"""自動更新が入っている PC か(自社PCの鍵 ~/.tyhld/bin/autoupdate/enabled・便 29babc5d)。
|
|
115
|
+
|
|
116
|
+
【なぜ住まいを受け取れる形にしたか(便 f20dd761 の重大発見2・便 08af46e0 で直した)】
|
|
117
|
+
住まいを中で決めていたので、試験が【実機の住まい】を覗いてしまっていた。
|
|
118
|
+
自動更新が入っているPCでは案内が「次の自動更新でそろいます」に変わるため、
|
|
119
|
+
同じ試験が機体によって通ったり落ちたりした。呼び手から渡せる形にして、
|
|
120
|
+
試験は使い捨ての住まいで判定する=どのPCでも同じ結果になる。
|
|
121
|
+
"""
|
|
122
|
+
base = os.path.expanduser('~') if home is None else home
|
|
123
|
+
return os.path.isfile(os.path.join(base, '.tyhld', 'bin', 'autoupdate', 'enabled'))
|
|
116
124
|
|
|
117
125
|
|
|
118
|
-
def recovery_steps(reason):
|
|
126
|
+
def recovery_steps(reason, home=None):
|
|
119
127
|
"""うまくいかなかった理由に【合った】手順を返す(便 675e2d05)。
|
|
120
128
|
|
|
121
129
|
【なぜ理由ごとに分けるか(実地 2026-09-20・便 8692c586 の重大発見1)】
|
|
122
130
|
以前は理由によらず「update-guard を打ってください」の固定文だった。version-drift
|
|
123
131
|
(入っている版と公開の版が違う)のときにこれを打っても、配り元の確認でまた弾かれる=
|
|
124
132
|
同じ失敗を繰り返すだけで直らない。先に入れ直しが要る。
|
|
133
|
+
|
|
134
|
+
home … 自動更新の鍵を探す住まい(既定は実機の住まい。試験は使い捨てを渡す)。
|
|
125
135
|
"""
|
|
126
136
|
if reason in ('flagged-bad', 'flagged-suspect'):
|
|
127
137
|
# ★管制で不具合・疑いの印が付いた版(便 29babc5d)。入れ直しても同じ版なら直らない。
|
|
128
|
-
if autoupdate_enabled():
|
|
138
|
+
if autoupdate_enabled(home):
|
|
129
139
|
return ['再開のしかた: この PC は自動更新が入っています。管制が直した版を決めれば、'
|
|
130
140
|
'次の自動更新で入れ替わります(人の操作は要りません)。']
|
|
131
141
|
return ['再開のしかた: この版には管制で不具合の印が付いています。CC1 に知らせてください'
|
|
@@ -134,7 +144,7 @@ def recovery_steps(reason):
|
|
|
134
144
|
# ★自動更新が入っている PC は、更新係が管制の決めた版へそろえる(便 29babc5d)。
|
|
135
145
|
# 人が npm で入れると段階配布の順番が崩れるので、入れ直しを促さない。
|
|
136
146
|
# ★not-installed・incomplete は配布物そのものが無い=更新係も動けないので、自動更新の有無によらず入れ直す。
|
|
137
|
-
if autoupdate_enabled() and reason in ('unpublished', 'deprecated', 'version-drift'):
|
|
147
|
+
if autoupdate_enabled(home) and reason in ('unpublished', 'deprecated', 'version-drift'):
|
|
138
148
|
return ['再開のしかた: この PC は自動更新が入っています。次の自動更新(管制が決めた版を、'
|
|
139
149
|
'作業中の便が終わってから入れます)でそろいます。管制の「PCの状態」で確かめてください。']
|
|
140
150
|
return [f'再開のしかた: この PC で次の順に打ってください。① {REINSTALL}',
|
|
@@ -153,7 +163,7 @@ def recovery_steps(reason):
|
|
|
153
163
|
return [f'再開のしかた: この PC で次の1行を打ってください。 {RESYNC}']
|
|
154
164
|
|
|
155
165
|
|
|
156
|
-
def build_payload(rec, env=None):
|
|
166
|
+
def build_payload(rec, env=None, home=None):
|
|
157
167
|
"""カードの中身。★何が起きていて、何をすれば直るかを日本語で書く。
|
|
158
168
|
|
|
159
169
|
★本文は【その回の控え(rec)の実際の値】だけから組み立てる。固定の版番号や、
|
|
@@ -165,7 +175,7 @@ def build_payload(rec, env=None):
|
|
|
165
175
|
kind = notice_kind(rec)
|
|
166
176
|
lines = [
|
|
167
177
|
f'いま止まっているもの: この PC({machine})で、{kind["stopped"]}',
|
|
168
|
-
*recovery_steps(reason),
|
|
178
|
+
*recovery_steps(reason, home),
|
|
169
179
|
'★このカードは管制からは押せません(この PC での操作が要ります)。',
|
|
170
180
|
f'PC: {machine}/うまくいかなかった理由: {reason}',
|
|
171
181
|
f'最後に試した時刻: {rec.get("at", "不明")}',
|
|
@@ -231,13 +241,15 @@ def _close(cfg, datalink, cards, key, path=None):
|
|
|
231
241
|
return True
|
|
232
242
|
|
|
233
243
|
|
|
234
|
-
def run(last_path, cfg=None, datalink=hook_datalink, state_path=None):
|
|
244
|
+
def run(last_path, cfg=None, datalink=hook_datalink, state_path=None, home=None):
|
|
235
245
|
"""返り値: 出したカードのID / None(出さなかった・閉じただけ)。
|
|
236
246
|
|
|
237
247
|
★失敗の回 … 前に出した札を【閉じてから】出し直す。管制は同じ鍵の再登録を寿命の延長
|
|
238
248
|
として扱い、本文は最初のままになる(実地 2026-09-20・札に 4日前の版が出ていた)。
|
|
239
249
|
閉じてから出し直せば、本文は必ずその回の値になる。
|
|
240
250
|
★成功の回 … 出していた札を閉じる。直ったのに札が最大60分出たままにならない。
|
|
251
|
+
|
|
252
|
+
home … 自動更新の鍵を探す住まい(既定は実機の住まい。試験は使い捨てを渡す)。
|
|
241
253
|
"""
|
|
242
254
|
rec = parse_last(last_path)
|
|
243
255
|
status = rec.get('status')
|
|
@@ -256,7 +268,7 @@ def run(last_path, cfg=None, datalink=hook_datalink, state_path=None):
|
|
|
256
268
|
|
|
257
269
|
# 失敗。★古い本文の札が残っていたら先に閉じる(上書きできない作りへの対処)。
|
|
258
270
|
_close(cfg, datalink, cards, key, state_path)
|
|
259
|
-
reg = datalink.register(cfg, build_payload(rec))
|
|
271
|
+
reg = datalink.register(cfg, build_payload(rec, home=home))
|
|
260
272
|
if not reg or not reg.get('id'):
|
|
261
273
|
return None
|
|
262
274
|
cards[key] = reg['id']
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
(i) 成功の回は【出していた札を閉じる】=直ったのに札が最大60分出たままにならない。
|
|
24
24
|
(j) 再開の手順が【うまくいかなかった理由に合っている】=同じ失敗を繰り返させない。
|
|
25
25
|
版のずれに「配り直してください」とだけ言っても、配り元の確認でまた弾かれる。
|
|
26
|
+
|
|
27
|
+
【★便 08af46e0 で塞いだ穴(便 f20dd761 の重大発見2)】
|
|
28
|
+
(k) 自動更新が入っているかの判定は【呼び手が渡した住まい】だけを見る。
|
|
29
|
+
以前は中で実機の住まい(~/.tyhld/bin/autoupdate/enabled)を覗いていたので、
|
|
30
|
+
自動更新が入ったPCでは案内の文が変わり、(j) の判定が機体ごとに通ったり落ちたりした。
|
|
31
|
+
この試験は使い捨ての住まいを渡すので、どのPCでも同じ結果になる。
|
|
26
32
|
"""
|
|
27
33
|
import json
|
|
28
34
|
import os
|
|
@@ -279,12 +285,19 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
279
285
|
# ── (j) 再開の手順が理由に合っている ────────────────────────────────────────
|
|
280
286
|
print('\n【(j) 再開の手順が、うまくいかなかった理由に合っている】')
|
|
281
287
|
with tempfile.TemporaryDirectory() as tmp:
|
|
282
|
-
|
|
288
|
+
# ★自動更新の鍵を探す住まいは、必ず【この回だけの使い捨て】を渡す(便 08af46e0)。
|
|
289
|
+
# 既定は実機の住まい(~/.tyhld/bin/autoupdate/enabled)なので、渡さないと
|
|
290
|
+
# 自動更新が入っているPCでは案内が別の文に変わり、同じ試験が機体によって落ちる
|
|
291
|
+
# (便 f20dd761 の重大発見2)。下の PLAIN_HOME は鍵を置かない=自動更新なしのPC。
|
|
292
|
+
PLAIN_HOME = os.path.join(tmp, 'home-plain')
|
|
293
|
+
os.makedirs(PLAIN_HOME, exist_ok=True)
|
|
294
|
+
|
|
295
|
+
def body_for(reason, what='dist-source', home=PLAIN_HOME):
|
|
283
296
|
line = (f'2026-09-20T09:00:00+0900 status=failed reason={reason} what={what} '
|
|
284
297
|
'version=0.12.6 published=0.12.31 root=/n/@tyhld/conductor')
|
|
285
298
|
link = FakeLink()
|
|
286
299
|
sn.run(write(tmp, f'{reason}.last', line + '\n'), datalink=link,
|
|
287
|
-
state_path=state(tmp, f'{reason}.json'))
|
|
300
|
+
state_path=state(tmp, f'{reason}.json'), home=home)
|
|
288
301
|
return link.sent[0]['question']
|
|
289
302
|
|
|
290
303
|
drift = body_for('version-drift')
|
|
@@ -332,21 +345,33 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
332
345
|
|
|
333
346
|
# ★自動更新が入っている PC(鍵 ~/.tyhld/bin/autoupdate/enabled)は、人に npm で入れさせない
|
|
334
347
|
# (更新係が管制の決めた版へそろえる。人が入れると段階配布の順番が崩れる)。
|
|
335
|
-
|
|
336
|
-
os.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
# ★判定する住まいは使い捨てを渡す(実機の住まいを覗かない=どのPCでも同じ結果)。
|
|
349
|
+
AUTO_HOME = os.path.join(tmp, 'home-auto')
|
|
350
|
+
os.makedirs(os.path.join(AUTO_HOME, '.tyhld', 'bin', 'autoupdate'), exist_ok=True)
|
|
351
|
+
write(os.path.join(AUTO_HOME, '.tyhld', 'bin', 'autoupdate'), 'enabled', 'x\n')
|
|
352
|
+
auto = body_for('unpublished', home=AUTO_HOME)
|
|
353
|
+
check_true('自動更新の PC: 次の自動更新でそろうと書く', '次の自動更新' in auto)
|
|
354
|
+
check('自動更新の PC: npm で入れさせない', 'npm install -g' in auto, False)
|
|
355
|
+
check_true('自動更新の PC・不具合の印: 人の操作は要らないと書く',
|
|
356
|
+
'人の操作は要りません' in body_for('flagged-suspect', home=AUTO_HOME))
|
|
357
|
+
check_true('自動更新の PC でも、配布物が無いときは入れ直す',
|
|
358
|
+
'npm install -g' in body_for('not-installed', home=AUTO_HOME))
|
|
359
|
+
|
|
360
|
+
# ★判定は【渡した住まい】だけを見る=実機の住まいに鍵が在っても無くても結果が変わらない
|
|
361
|
+
# (便 f20dd761 の重大発見2 の根治。ここが穴だと、この試験は機体ごとに答えが変わる)。
|
|
362
|
+
check('鍵の無い住まい: 自動更新なしと判定', sn.autoupdate_enabled(PLAIN_HOME), False)
|
|
363
|
+
check('鍵の在る住まい: 自動更新ありと判定', sn.autoupdate_enabled(AUTO_HOME), True)
|
|
364
|
+
for fake_home in (PLAIN_HOME, AUTO_HOME, tmp):
|
|
365
|
+
old_home = os.environ.get('HOME')
|
|
366
|
+
os.environ['HOME'] = fake_home
|
|
367
|
+
try:
|
|
368
|
+
check(f'実機の住まいを覗かない(HOME={os.path.basename(fake_home)})',
|
|
369
|
+
sn.autoupdate_enabled(PLAIN_HOME), False)
|
|
370
|
+
finally:
|
|
371
|
+
if old_home is None:
|
|
372
|
+
os.environ.pop('HOME', None)
|
|
373
|
+
else:
|
|
374
|
+
os.environ['HOME'] = old_home
|
|
350
375
|
|
|
351
376
|
# ── ★札の覚え書き(壊れていても・書けなくても止まらない)──────────────────
|
|
352
377
|
print('\n【★札の覚え書き】')
|