@tyhld/conductor 0.12.101 → 0.12.103
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 +40 -1
- package/dist/relay.js +318 -8
- package/dist/selfcheck.js +28 -3
- package/package.json +1 -1
package/dist/autoupdate.js
CHANGED
|
@@ -520,6 +520,40 @@ function waitingForHuman(d, now) {
|
|
|
520
520
|
detail: `自己診断を測り直しましたが、まだ通りません(${r.failedKeys.join(',') || '不明'}${r.humanOnly ? '・人の操作待ち' : ''})`,
|
|
521
521
|
};
|
|
522
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
|
+
}
|
|
523
557
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
524
558
|
// 1回ぶん
|
|
525
559
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -579,6 +613,10 @@ export async function runUpdater(d) {
|
|
|
579
613
|
// ③-2 人の操作待ち(🔵)で自己診断が落ちたままなら、測り直してから先へ進む(便 4cf71dde)。
|
|
580
614
|
// ★落ちたまま入れ替えると、同じ理由で落ちて戻し、版に疑いが付く(実物 2026-10-03 02:43Z・ノートPC)。
|
|
581
615
|
const human = waitingForHuman(d, now);
|
|
616
|
+
// ③-3 仕組みの異常(🟡)で自己診断が落ちたままなら、held と同じ間隔で測り直す(便 cbc27db6)。
|
|
617
|
+
// ★その場かぎりの時間切れで落ちた FAIL が翌日まで残り、この機が順番から外れたままになるのを直す。
|
|
618
|
+
// ★入れ替えるかどうかの決め方は1つも変えない(通らなければ今までどおり FAIL のまま)。
|
|
619
|
+
const refault = recheckAfterFault(d, now);
|
|
582
620
|
// ④ どの部品を動かすか(★conductor の目標が今と違うあいだは Claude Code を動かさない)。
|
|
583
621
|
const claudeNow = d.resolveClaude();
|
|
584
622
|
let comp = null;
|
|
@@ -614,7 +652,8 @@ export async function runUpdater(d) {
|
|
|
614
652
|
// ★後続の機は候補を探さない・候補の自己診断を流さない(役は管制が決める・欄が無ければ今までどおり)。
|
|
615
653
|
if (target.role !== 'follower')
|
|
616
654
|
await reportCandidates(d, s, installed, claudeNow?.version ?? null);
|
|
617
|
-
|
|
655
|
+
const same = `目標の版と同じです(conductor ${installed ?? '不明'}・Claude Code ${claudeNow?.version ?? '不明'})`;
|
|
656
|
+
return done(d, s, 'none', refault ? `${same}。${refault}` : same);
|
|
618
657
|
}
|
|
619
658
|
if (human.waiting) {
|
|
620
659
|
removeDrain(d, s);
|
package/dist/relay.js
CHANGED
|
@@ -316,6 +316,111 @@ export const RELAY_SKIP_WARN_THRESHOLD = 6;
|
|
|
316
316
|
* 中央が便を抱える上限(CENTRAL_BUSY_HOLD_MAX_MS=3時間)と同じ値にそろえる。
|
|
317
317
|
*/
|
|
318
318
|
export const RELAY_CODEX_TURN_HOLD_MAX_MS = CENTRAL_BUSY_HOLD_MAX_MS;
|
|
319
|
+
/**
|
|
320
|
+
* 配り役が現場ごとの控えを置く場所(★再起動で消えない・報告の置き場と同じ親の下)。
|
|
321
|
+
* 環境変数 `CONDUCTOR_RELAY_STATE_DIR` を指定したときはそこだけ(試験・特殊な機体向け)。
|
|
322
|
+
*/
|
|
323
|
+
export function relayStateDir(env = process.env) {
|
|
324
|
+
const explicit = (env.CONDUCTOR_RELAY_STATE_DIR ?? '').trim();
|
|
325
|
+
if (explicit)
|
|
326
|
+
return explicit;
|
|
327
|
+
const home = env.HOME ?? os.homedir();
|
|
328
|
+
const state = (env.XDG_STATE_HOME ?? '').trim() || path.join(home, '.local', 'state');
|
|
329
|
+
return path.join(state, 'conductor', 'relay');
|
|
330
|
+
}
|
|
331
|
+
/** この現場の「Codex のターンの札」の控えのパス。 */
|
|
332
|
+
export function codexTurnFile(site, env = process.env) {
|
|
333
|
+
const safe = String(site ?? '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 128) || 'unknown';
|
|
334
|
+
return path.join(relayStateDir(env), `codex-turn-${safe}.json`);
|
|
335
|
+
}
|
|
336
|
+
/** 控えを読む。無い・読めない・形が違うときは null(★配り役を止めない)。 */
|
|
337
|
+
export function readCodexTurnRecord(site, env = process.env) {
|
|
338
|
+
try {
|
|
339
|
+
const o = JSON.parse(readFileSync(codexTurnFile(site, env), 'utf8'));
|
|
340
|
+
const jobId = typeof o.jobId === 'string' && o.jobId.trim() !== '' ? o.jobId.trim() : null;
|
|
341
|
+
const since = typeof o.since === 'number' && Number.isFinite(o.since) ? o.since : null;
|
|
342
|
+
return {
|
|
343
|
+
jobId,
|
|
344
|
+
since: jobId === null ? null : since,
|
|
345
|
+
noticeSeen: o.noticeSeen === true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** 控えを書く(★書けなくても配り役は止まらない=今までどおりメモリの札で動く)。 */
|
|
353
|
+
export function writeCodexTurnRecord(site, rec, env = process.env) {
|
|
354
|
+
const file = codexTurnFile(site, env);
|
|
355
|
+
try {
|
|
356
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
357
|
+
writeFileSync(file, `${JSON.stringify(rec)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* 「Codex の前のターンの札」を下ろしてよいかを決める純粋関数(I/Oなし=単体テスト可能)。
|
|
366
|
+
*
|
|
367
|
+
* 【なぜ知らせ1本に頼らないか(調査 b9f6f53e の重大発見1・2)】札が下りる唯一の道は
|
|
368
|
+
* 「終わりの知らせが受け皿に届くこと」だった。実測 2026-10-05、走行中のターンへ人が
|
|
369
|
+
* 手で文を入れた便(9fb59241)では知らせが1行も出ず、現場が最長3時間ふさがった。
|
|
370
|
+
* そこで出口を1本足す:★画面が待機(作業中の印が無い)で、かつその便が管制で終端
|
|
371
|
+
* (done / failed / timeout など)なら、ターンは終わっている。
|
|
372
|
+
* ★画面が作業中に見えるあいだは下ろさない=長いターンの途中で次の便を割り込ませない
|
|
373
|
+
* (この札の本来の目的は1バイトも弱めない)。
|
|
374
|
+
* ★画面が読めない・管制に聞けないときも下ろさない(断定しない=保守側)。
|
|
375
|
+
*/
|
|
376
|
+
export function decideCodexTurnRelease(input) {
|
|
377
|
+
if (input.heldMs >= RELAY_CODEX_TURN_HOLD_MAX_MS)
|
|
378
|
+
return 'release-max';
|
|
379
|
+
if (input.screenIdle === true && input.centralFinished === true)
|
|
380
|
+
return 'release-finished';
|
|
381
|
+
return 'hold';
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 札の控えを読み込む(★起動後の最初の1周だけ・Codex の現場だけ)。
|
|
385
|
+
* 控えに「まだ途中のターン」が在れば、メモリの札として引き継ぐ=配り役を入れ直しても
|
|
386
|
+
* 長いターンの途中に次の便を割り込ませない。受け口の知らせを見た事実も引き継ぐ。
|
|
387
|
+
*/
|
|
388
|
+
export function loadCodexTurnOnce(cfg, state, env = process.env, logLine = log) {
|
|
389
|
+
if (state.codexTurnLoaded)
|
|
390
|
+
return;
|
|
391
|
+
state.codexTurnLoaded = true;
|
|
392
|
+
if (kindOf(cfg) !== 'codex')
|
|
393
|
+
return;
|
|
394
|
+
const rec = readCodexTurnRecord(cfg.site, env);
|
|
395
|
+
if (rec === null)
|
|
396
|
+
return;
|
|
397
|
+
state.codexTurnDisk = JSON.stringify(rec);
|
|
398
|
+
if (rec.noticeSeen)
|
|
399
|
+
state.codexNoticeSeen = true;
|
|
400
|
+
if (rec.jobId !== null && state.codexTurnOpen === null) {
|
|
401
|
+
state.codexTurnOpen = { jobId: rec.jobId, since: rec.since ?? Date.now() };
|
|
402
|
+
logLine(`site=${cfg.site} id=${rec.jobId} のターンは控えでまだ途中でした` +
|
|
403
|
+
'(札を引き継ぎます=入れ直しても状態が食い違わない)');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** いまの札を控えへ写す(★中身が変わっていなければ書かない)。書けなくても配り役は止まらない。 */
|
|
407
|
+
export function saveCodexTurn(cfg, state, env = process.env) {
|
|
408
|
+
if (kindOf(cfg) !== 'codex')
|
|
409
|
+
return;
|
|
410
|
+
const rec = {
|
|
411
|
+
jobId: state.codexTurnOpen?.jobId ?? null,
|
|
412
|
+
since: state.codexTurnOpen?.since ?? null,
|
|
413
|
+
noticeSeen: state.codexNoticeSeen,
|
|
414
|
+
};
|
|
415
|
+
const text = JSON.stringify(rec);
|
|
416
|
+
if (text === state.codexTurnDisk)
|
|
417
|
+
return;
|
|
418
|
+
// まだ控えが無く、書くことも何も無い(札なし・知らせ未観測)=ファイルを作らない。
|
|
419
|
+
if (state.codexTurnDisk === null && rec.jobId === null && !rec.noticeSeen)
|
|
420
|
+
return;
|
|
421
|
+
if (writeCodexTurnRecord(cfg.site, rec, env))
|
|
422
|
+
state.codexTurnDisk = text;
|
|
423
|
+
}
|
|
319
424
|
/**
|
|
320
425
|
* 受け皿の名前をそろえる仕事(登録簿の書き出し・読まれない受け皿の始末)の間隔。
|
|
321
426
|
* ★配達の判断には1バイトも関わらない。フォルダを見に行くので毎周はやらない。
|
|
@@ -342,6 +447,8 @@ export function createRelayState() {
|
|
|
342
447
|
lastNudgeLogAt: null,
|
|
343
448
|
codexTurnOpen: null,
|
|
344
449
|
codexNoticeSeen: false,
|
|
450
|
+
codexTurnLoaded: false,
|
|
451
|
+
codexTurnDisk: null,
|
|
345
452
|
codexTrayTidiedAt: null,
|
|
346
453
|
central: { failures: 0, detail: '', downSince: null },
|
|
347
454
|
readiness: createReadiness(),
|
|
@@ -1163,6 +1270,89 @@ export function takeCodexNotice(site, env = process.env) {
|
|
|
1163
1270
|
const all = takeCodexNotices(site, env);
|
|
1164
1271
|
return all.length === 0 ? null : all[all.length - 1];
|
|
1165
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* 受け皿から【便の印がこの便と一致する知らせ】だけを取り出す(★ほかの行は受け皿に残す)。
|
|
1275
|
+
*
|
|
1276
|
+
* 【なぜ「全件を空にする」takeCodexNotices と別に要るか(便 b4747dd7)】
|
|
1277
|
+
* 見張り(keepalive)へ移った便の報告本文を拾うために受け皿を見るが、そのとき受け皿には
|
|
1278
|
+
* 【いま追跡中(pending)の別の便の知らせ】が混ざっていることがある。全件を空にすると、
|
|
1279
|
+
* 追跡中の便の知らせを横から奪ってその便の報告を落としてしまう。だから一致した行だけを
|
|
1280
|
+
* 取り、残りはそのまま受け皿に戻す(★本線の pickCodexNoticeFor の取り分を1行も減らさない)。
|
|
1281
|
+
* ★一致の判定は【便の印(jobId)】だけ。印が無い行は取らない(別の便の知らせを取り違えない)。
|
|
1282
|
+
* ★書きかけの行は取らない・消さない(takeCodexNotices と同じ守り)。
|
|
1283
|
+
*/
|
|
1284
|
+
export function takeCodexNoticesForJobId(site, commandId, env = process.env) {
|
|
1285
|
+
const dir = codexNotifyDir(env);
|
|
1286
|
+
if (!dir)
|
|
1287
|
+
return [];
|
|
1288
|
+
const file = path.join(dir, `${site}.jsonl`);
|
|
1289
|
+
let raw;
|
|
1290
|
+
try {
|
|
1291
|
+
raw = readFileSync(file, 'utf8');
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return []; // まだ1件も来ていない
|
|
1295
|
+
}
|
|
1296
|
+
if (hasPartialJsonlTail(raw)) {
|
|
1297
|
+
let writing = false;
|
|
1298
|
+
try {
|
|
1299
|
+
writing = Date.now() - statSync(file).mtimeMs < CODEX_NOTICE_PARTIAL_WAIT_MS;
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
writing = false; // 時刻が読めなければ、今までどおり進む
|
|
1303
|
+
}
|
|
1304
|
+
if (writing)
|
|
1305
|
+
return []; // 書き終わってから、次の回で拾う
|
|
1306
|
+
}
|
|
1307
|
+
const whole = completeJsonlLines(raw);
|
|
1308
|
+
// 末尾の書きかけ(改行で終わっていない残り)は、そのまま受け皿に残す。
|
|
1309
|
+
const partial = hasPartialJsonlTail(raw) ? raw.slice(raw.lastIndexOf('\n') + 1) : '';
|
|
1310
|
+
const mine = [];
|
|
1311
|
+
const keep = [];
|
|
1312
|
+
for (const line of whole) {
|
|
1313
|
+
const t = line.trim();
|
|
1314
|
+
if (t === '')
|
|
1315
|
+
continue;
|
|
1316
|
+
let o = null;
|
|
1317
|
+
try {
|
|
1318
|
+
o = JSON.parse(t);
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
o = null;
|
|
1322
|
+
}
|
|
1323
|
+
const jobId = typeof o?.jobId === 'string' ? o.jobId.trim() : '';
|
|
1324
|
+
if (o !== null &&
|
|
1325
|
+
o.site === site &&
|
|
1326
|
+
jobId !== '' &&
|
|
1327
|
+
codexReportIdMatches(jobId, commandId)) {
|
|
1328
|
+
mine.push({
|
|
1329
|
+
site,
|
|
1330
|
+
report: typeof o.report === 'string' ? o.report : '',
|
|
1331
|
+
threadId: o.threadId ?? null,
|
|
1332
|
+
turnId: o.turnId ?? null,
|
|
1333
|
+
jobId,
|
|
1334
|
+
at: parseNoticeAt(o.at),
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
keep.push(line); // ★自分の物でない行は1字も変えずに受け皿へ戻す
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (mine.length === 0)
|
|
1342
|
+
return []; // 何も取らなかった=受け皿も書き換えない
|
|
1343
|
+
const rest = keep.length === 0 && partial === '' ? '' : `${keep.map((l) => `${l}\n`).join('')}${partial}`;
|
|
1344
|
+
try {
|
|
1345
|
+
if (rest === '')
|
|
1346
|
+
unlinkSync(file);
|
|
1347
|
+
else
|
|
1348
|
+
writeFileSync(file, rest, { encoding: 'utf8', mode: 0o600 });
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
// 書き戻せなくても、取った知らせはそのまま使う(完了を落とさない)。
|
|
1352
|
+
// ★同じ行をもう一度読んでも、完了の書き込み自体は冪等(patchStatus / completeWithReport)。
|
|
1353
|
+
}
|
|
1354
|
+
return mine;
|
|
1355
|
+
}
|
|
1166
1356
|
/** 控えに残す行数の上限(古い物から落とす=受け皿の隣で際限なく育たせない)。 */
|
|
1167
1357
|
export const CODEX_SKIPPED_NOTICE_MAX_LINES = 200;
|
|
1168
1358
|
/**
|
|
@@ -1759,6 +1949,41 @@ export function hasStandaloneDone(text, site) {
|
|
|
1759
1949
|
const lines = text.replace(/\r/g, '').split('\n');
|
|
1760
1950
|
return lines.some((line) => isStandaloneDoneLine(line, marker));
|
|
1761
1951
|
}
|
|
1952
|
+
/** テキスト中の「DONE:{site} 単独行」の数(★床(doneFloor)の基準に使う)。 */
|
|
1953
|
+
export function countStandaloneDone(text, site) {
|
|
1954
|
+
const marker = `DONE:${site}`;
|
|
1955
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
1956
|
+
let n = 0;
|
|
1957
|
+
for (const line of lines)
|
|
1958
|
+
if (isStandaloneDoneLine(line, marker))
|
|
1959
|
+
n += 1;
|
|
1960
|
+
return n;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* 追跡中(pending)の便について、画面から完了の合図を読む(★I/Oなし=単体テスト可能)。
|
|
1964
|
+
*
|
|
1965
|
+
* 【3つの道(上から順に効く)】
|
|
1966
|
+
* ① アンカーが画面に見える … 今までどおり「アンカーより下」だけを見る(Claude の道・1文字も変えない)。
|
|
1967
|
+
* ② アンカーが見えず、床が無い … 今までどおり(wide なら全画面/そうでなければ false)。
|
|
1968
|
+
* ③ アンカーが見えず、床が在る(★Codex・便 b4747dd7)… 全画面の `DONE:{site}` 単独行を数え、
|
|
1969
|
+
* 【配った時点の数(床)より増えていたら】完了とみなす。増えていなければ、観測した数まで
|
|
1970
|
+
* 床を下げる(古い DONE 行が履歴から流れたぶんだけ下がる=そのあと出た合図を取りこぼさない)。
|
|
1971
|
+
*
|
|
1972
|
+
* ★床を上げないのが要。上げてしまうと、この便が出した DONE 行が床に混ざり、完了が永久に成立しない。
|
|
1973
|
+
*/
|
|
1974
|
+
export function scanDoneForPending(cap, site, anchor, opts) {
|
|
1975
|
+
const below = sliceBelowAnchor(cap, anchor);
|
|
1976
|
+
if (below !== null)
|
|
1977
|
+
return { done: hasStandaloneDone(below, site), floor: opts.doneFloor };
|
|
1978
|
+
if (!opts.wide)
|
|
1979
|
+
return { done: false, floor: opts.doneFloor };
|
|
1980
|
+
if (opts.doneFloor === null)
|
|
1981
|
+
return { done: hasStandaloneDone(cap, site), floor: null };
|
|
1982
|
+
const seen = countStandaloneDone(cap, site);
|
|
1983
|
+
if (seen > opts.doneFloor)
|
|
1984
|
+
return { done: true, floor: opts.doneFloor };
|
|
1985
|
+
return { done: false, floor: Math.min(opts.doneFloor, seen) };
|
|
1986
|
+
}
|
|
1762
1987
|
/**
|
|
1763
1988
|
* アンカー基準の完了判定(根治の中核)。
|
|
1764
1989
|
* 「アンカーより下の領域」に DONE:{site} 単独行があれば完了とみなす。
|
|
@@ -3491,6 +3716,8 @@ export async function fetchControl(cfg) {
|
|
|
3491
3716
|
return {
|
|
3492
3717
|
relayHalted: true,
|
|
3493
3718
|
ok: false,
|
|
3719
|
+
// ★例外で落ちた=時間切れ・回線の失敗。断られた答え(HTTP 非2xx)はここへ来ない(上で返している)。
|
|
3720
|
+
transient: true,
|
|
3494
3721
|
detail: err instanceof Error ? err.message : String(err),
|
|
3495
3722
|
};
|
|
3496
3723
|
}
|
|
@@ -4328,6 +4555,8 @@ const DEFAULT_KEEPALIVE_DEPS = {
|
|
|
4328
4555
|
capture: (site) => tmuxCapture(site),
|
|
4329
4556
|
heartbeat: (cfg, id, busy) => sendHeartbeat(cfg, id, busy === true),
|
|
4330
4557
|
complete: (cfg, id, report) => patchStatus(cfg, id, 'done', report),
|
|
4558
|
+
completeFromFile: (cfg, id) => completeWithReport(cfg, id),
|
|
4559
|
+
takeNotices: (site, id) => takeCodexNoticesForJobId(site, id),
|
|
4331
4560
|
centralStatus: (cfg, id) => fetchCommandStatus(cfg, id),
|
|
4332
4561
|
now: () => Date.now(),
|
|
4333
4562
|
logLine: (msg) => log(msg),
|
|
@@ -4342,10 +4571,53 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
4342
4571
|
const ka = state.keepalive;
|
|
4343
4572
|
if (!ka)
|
|
4344
4573
|
return;
|
|
4345
|
-
const { capture, heartbeat, complete, centralStatus: getStatus, now: clock, logLine } = {
|
|
4574
|
+
const { capture, heartbeat, complete, completeFromFile, takeNotices, centralStatus: getStatus, now: clock, logLine, } = {
|
|
4346
4575
|
...DEFAULT_KEEPALIVE_DEPS,
|
|
4347
4576
|
...deps,
|
|
4348
4577
|
};
|
|
4578
|
+
// ── 見張りに移った便の「終わりの知らせ」を取り込む(★Codex・便 b4747dd7) ──
|
|
4579
|
+
// 15分を超えたターンの便は、追跡(pending)が切れたあとに知らせが届く。従来はその知らせを
|
|
4580
|
+
// 「追っている便が無い」として控えへ回していたので、報告本文が1文字も管制に載らなかった
|
|
4581
|
+
// (実測 2026-10-05: f7059eb5 は 15分13秒・dc60bf38 は 19分24秒で、どちらも本文なし)。
|
|
4582
|
+
// ★取るのは【便の印がこの便と一致する行】だけ。ほかの行は受け皿に残す(追跡中の便の取り分を奪わない)。
|
|
4583
|
+
if (kindOf(cfg) === 'codex') {
|
|
4584
|
+
const mine = takeNotices(ka.site, ka.id);
|
|
4585
|
+
// 2件以上あれば【新しい方】を使う(受け皿は古い順)。
|
|
4586
|
+
const notice = mine.length === 0 ? null : mine[mine.length - 1];
|
|
4587
|
+
if (notice !== null) {
|
|
4588
|
+
state.codexNoticeSeen = true;
|
|
4589
|
+
// ★この便のターンは終わった=「前のターンが途中」の札も下ろす。
|
|
4590
|
+
if (state.codexTurnOpen !== null && state.codexTurnOpen.jobId === ka.id) {
|
|
4591
|
+
state.codexTurnOpen = null;
|
|
4592
|
+
logLine(`site=${cfg.site} id=${ka.id} のターンが終わったと分かりました(次の便を配れます)`);
|
|
4593
|
+
}
|
|
4594
|
+
const intake = intakeCodexReport(notice.report, { commandId: ka.id, site: cfg.site });
|
|
4595
|
+
logLine(`site=${cfg.site} id=${ka.id} ${intake.note}`);
|
|
4596
|
+
const body = notice.report.trim().length > 0 ? notice.report : null;
|
|
4597
|
+
const ok = intake.wrote
|
|
4598
|
+
? await completeFromFile(cfg, ka.id)
|
|
4599
|
+
: await complete(cfg, ka.id, body);
|
|
4600
|
+
if (!ok)
|
|
4601
|
+
return; // 書けなければ畳まない(次の周でやり直す。知らせは使い切っている=囲みは報告ファイルに残る)
|
|
4602
|
+
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 追跡が切れたあとに届いた終わりの知らせで完了を報告` +
|
|
4603
|
+
`(印が一致${intake.wrote ? '・囲みから報告ファイル' : body !== null ? '・報告添付' : ''})`);
|
|
4604
|
+
await deliverJobEndFacts(cfg, {
|
|
4605
|
+
commandId: ka.id,
|
|
4606
|
+
site: cfg.site,
|
|
4607
|
+
finishedAt: new Date().toISOString(),
|
|
4608
|
+
endKind: 'completed',
|
|
4609
|
+
exitCode: null,
|
|
4610
|
+
exitSignal: null,
|
|
4611
|
+
reportDelivered: body != null || reportDelivery(ka.id).delivered,
|
|
4612
|
+
processEndObserved: false,
|
|
4613
|
+
detail: '配り役が完了の知らせで確かめた(追跡が切れたあとに届いた知らせ・職人の処理は中継が持っていない)' +
|
|
4614
|
+
(intake.wrote ? '/最後の返事の囲みから報告ファイルを組み立てた' : ''),
|
|
4615
|
+
}, { logLine });
|
|
4616
|
+
noteBusyEdge(state, cfg.site, ka.id, false, logLine);
|
|
4617
|
+
state.keepalive = null;
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4349
4621
|
// ★窓の持ち主が替わったら、画面からの完了検出はやめる(取り違えた完了を構造的に防ぐ)。
|
|
4350
4622
|
// 別の指示が同じ窓で動くと、その出力も古いアンカーより下に並ぶため、そのまま読むと
|
|
4351
4623
|
// 新しい指示の DONE 行と報告を、見張っている古い指示の完了として送ってしまう。
|
|
@@ -4544,6 +4816,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
4544
4816
|
// 次のtick間隔が自動的に通常(15秒)へ戻る。★判定ロジックはこの値を一切参照しない(観測専用)。
|
|
4545
4817
|
state.promptPending = false;
|
|
4546
4818
|
try {
|
|
4819
|
+
// ── Codex のターンの札の控えを読み込む(★起動後1回・便 b4747dd7) ──
|
|
4820
|
+
// 配り役を入れ直しても、長いターンの途中に次の便を割り込ませない(loadCodexTurnOnce の説明)。
|
|
4821
|
+
loadCodexTurnOnce(cfg, state);
|
|
4547
4822
|
// ── sent孤児の復元(2-b・起動後1回) ──
|
|
4548
4823
|
// cli.ts を触らず「起動時1回」を実現するため、最初の tick でだけ復元する。
|
|
4549
4824
|
// 失敗しても再試行しない(フラグを先に立てる=起動を妨げない/多重実行防止)。
|
|
@@ -4698,8 +4973,22 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
4698
4973
|
return;
|
|
4699
4974
|
}
|
|
4700
4975
|
}
|
|
4701
|
-
|
|
4702
|
-
|
|
4976
|
+
// ★Codex はアンカーが画面に出ない(長い貼り付けを畳んで表示する)ので、床(doneFloor)を
|
|
4977
|
+
// 使って「配った時点より DONE 行が増えたか」で見る(scanDoneForPending の説明・便 b4747dd7)。
|
|
4978
|
+
// Claude は従来どおり「アンカーより下」だけを見る=動きは1バイトも変わらない。
|
|
4979
|
+
if (state.pending.doneFloor != null && state.pending.doneFloorSettled !== true) {
|
|
4980
|
+
// 配達のエコーを含んだ数も床に取り込む(PendingEntry.doneFloorSettled の説明)。
|
|
4981
|
+
state.pending.doneFloor = Math.max(state.pending.doneFloor, countStandaloneDone(cap, cfg.site));
|
|
4982
|
+
state.pending.doneFloorSettled = true;
|
|
4983
|
+
}
|
|
4984
|
+
const scan = scanDoneForPending(cap, cfg.site, state.pending.anchor, {
|
|
4985
|
+
wide: state.pending.screenDoneWide,
|
|
4986
|
+
doneFloor: state.pending.doneFloor ?? null,
|
|
4987
|
+
});
|
|
4988
|
+
if (state.pending.doneFloor != null && scan.floor !== state.pending.doneFloor) {
|
|
4989
|
+
state.pending.doneFloor = scan.floor;
|
|
4990
|
+
}
|
|
4991
|
+
if (scan.done) {
|
|
4703
4992
|
// ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
|
|
4704
4993
|
// 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
4705
4994
|
// ★「届いた」は形まで見る(便 461be339)。整っていなければ理由も一緒に届ける。
|
|
@@ -4958,14 +5247,26 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
4958
5247
|
if (kindOf(cfg) === 'codex' && state.codexTurnOpen !== null && state.codexNoticeSeen) {
|
|
4959
5248
|
const open = state.codexTurnOpen;
|
|
4960
5249
|
const held = Date.now() - open.since;
|
|
4961
|
-
|
|
5250
|
+
// ★知らせ1本に頼らない出口(便 b4747dd7・調査 b9f6f53e の直す案②)。
|
|
5251
|
+
// 画面が待機で、かつその便が管制で終端なら、ターンは終わっている=札を下ろして配る。
|
|
5252
|
+
// ★画面が作業中に見えるあいだは下ろさない(長いターンの途中に割り込まない守りは残す)。
|
|
5253
|
+
const holdCap = (hooks?.capture ?? tmuxCapture)(cfg.site);
|
|
5254
|
+
const screenIdle = holdCap === null ? null : looksIdleFor(holdCap, kindOf(cfg));
|
|
5255
|
+
const centralFinished = screenIdle === true ? isCentralFinished(await fetchCommandStatus(cfg, open.jobId)) : null;
|
|
5256
|
+
const release = decideCodexTurnRelease({ heldMs: held, screenIdle, centralFinished });
|
|
5257
|
+
if (release === 'hold') {
|
|
4962
5258
|
noteReadiness(state, 'busy', cmd.id);
|
|
4963
5259
|
log(`site=${cfg.site} id=${cmd.id} 前の便(id=${open.jobId})のターンがまだ終わっていないので配りません` +
|
|
4964
|
-
`(${Math.round(held / 1000)}
|
|
5260
|
+
`(${Math.round(held / 1000)}秒待っています・画面=` +
|
|
5261
|
+
`${screenIdle === null ? '読めない' : screenIdle ? '待機' : '作業中'}` +
|
|
5262
|
+
`${screenIdle === true ? '・管制=終端でない' : ''})`);
|
|
4965
5263
|
return;
|
|
4966
5264
|
}
|
|
4967
|
-
log(
|
|
4968
|
-
|
|
5265
|
+
log(release === 'release-finished'
|
|
5266
|
+
? `site=${cfg.site} id=${cmd.id} 前の便(id=${open.jobId})は画面が待機で管制でも終端です` +
|
|
5267
|
+
'→札を下ろして配ります(終わりの知らせは来ていません)'
|
|
5268
|
+
: `WARN site=${cfg.site} id=${cmd.id} 前の便(id=${open.jobId})のターンの終わりの知らせが` +
|
|
5269
|
+
`${Math.round(RELAY_CODEX_TURN_HOLD_MAX_MS / 60000)}分来ないので、札を下ろして配ります`);
|
|
4969
5270
|
state.codexTurnOpen = null;
|
|
4970
5271
|
}
|
|
4971
5272
|
// ── アイドル判定(保守的・二重チェック) ──
|
|
@@ -5080,6 +5381,12 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
5080
5381
|
// 届く先が本物の職人の箱であることは、直前の tmuxHasSession / isIdleStable / looksLikeAgent が
|
|
5081
5382
|
// 確認済み。だから「送れた=配達できた」と扱ってよい。
|
|
5082
5383
|
const sentAt = Date.now();
|
|
5384
|
+
// ★Codex はアンカーが画面に出ない(長い貼り付けを畳んで表示する)=アンカーより下を見る
|
|
5385
|
+
// 完了検出が原理的に成立せず、毎回 15分の上限を待ち切っていた(調査 b9f6f53e・直す案①)。
|
|
5386
|
+
// 配達したという事実は send-keys の成否で確定しているので、見張りと同じ理屈で全画面を
|
|
5387
|
+
// 見てよい。取り違えを防ぐため【配った時点に見えていた DONE 行の数】を床として覚え、
|
|
5388
|
+
// 床より増えたときだけ完了とみなす(PendingEntry.doneFloor の説明)。
|
|
5389
|
+
const codexFloor = kindOf(cfg) === 'codex' ? countStandaloneDone(guardCap, cfg.site) : null;
|
|
5083
5390
|
state.pending = {
|
|
5084
5391
|
id: cmd.id,
|
|
5085
5392
|
site: cfg.site,
|
|
@@ -5088,7 +5395,8 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
5088
5395
|
body: cmd.body,
|
|
5089
5396
|
delivered: true,
|
|
5090
5397
|
sentMarked: false,
|
|
5091
|
-
screenDoneWide:
|
|
5398
|
+
screenDoneWide: codexFloor !== null,
|
|
5399
|
+
...(codexFloor !== null ? { doneFloor: codexFloor, doneFloorSettled: false } : {}),
|
|
5092
5400
|
...(useAck ? { markAs: 'delivering' } : {}),
|
|
5093
5401
|
};
|
|
5094
5402
|
// 累計配達回数を通算(絶対上限 RELAY_DELIVER_MAX の基準)。
|
|
@@ -5158,6 +5466,8 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
5158
5466
|
await sendAckAtCentral(cfg, state);
|
|
5159
5467
|
}
|
|
5160
5468
|
finally {
|
|
5469
|
+
// ★どの道で抜けても、札の中身を控えへ写す(中身が変わっていなければ書かない)。
|
|
5470
|
+
saveCodexTurn(cfg, state);
|
|
5161
5471
|
state.running = false;
|
|
5162
5472
|
}
|
|
5163
5473
|
}
|
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 を一時的に置き換える)。 */
|