@tyhld/conductor 0.12.80 → 0.12.81
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/cli.js +7 -0
- package/dist/relay.js +169 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -195,6 +195,9 @@ async function sendReport(cfg) {
|
|
|
195
195
|
body.undeliveredSec = d.undeliveredSec;
|
|
196
196
|
if (d.queuedCommandId !== null)
|
|
197
197
|
body.queuedCommandId = d.queuedCommandId;
|
|
198
|
+
// ★配った便の完了待ちの長さ('waiting-done' のときだけ)。管制が長すぎる待ちを見つけられる。
|
|
199
|
+
if (d.waitingSec !== null)
|
|
200
|
+
body.waitingSec = d.waitingSec;
|
|
198
201
|
}
|
|
199
202
|
// ★未処理の報告(まだ管制へ渡せていない控え)の指示IDを載せる(便 57c04e6d)。
|
|
200
203
|
// 1件も無ければ載せない=今までと同じ本文。多すぎるときは上限までにして総数を添える。
|
|
@@ -348,6 +351,10 @@ async function start(args) {
|
|
|
348
351
|
}
|
|
349
352
|
nudgeWatcher = watchNudges(cfg.site, () => {
|
|
350
353
|
console.log(`[relay] site=${cfg.site} 耳から合図 → 指示の取得を前倒しします`);
|
|
354
|
+
// ★合図が空振りしたときに理由を1行出せるよう、受けた時刻を relay へ渡す(案B-3)。
|
|
355
|
+
// relayTick が「前の周が走行中」「追跡中」で return するとき、しぼり付きで理由を記録する。
|
|
356
|
+
if (relayStateForReport)
|
|
357
|
+
relayStateForReport.nudgedAt = Date.now();
|
|
351
358
|
void relayRunner?.run();
|
|
352
359
|
});
|
|
353
360
|
}
|
package/dist/relay.js
CHANGED
|
@@ -239,6 +239,8 @@ export function createRelayState() {
|
|
|
239
239
|
abandoned: new Set(),
|
|
240
240
|
promptPending: false,
|
|
241
241
|
busyRaisedId: null,
|
|
242
|
+
nudgedAt: null,
|
|
243
|
+
lastNudgeLogAt: null,
|
|
242
244
|
central: { failures: 0, detail: '', downSince: null },
|
|
243
245
|
readiness: createReadiness(),
|
|
244
246
|
};
|
|
@@ -277,12 +279,58 @@ export function noteReadiness(state, next, commandId, now = Date.now()) {
|
|
|
277
279
|
export function deliveryReport(state, now = Date.now()) {
|
|
278
280
|
const r = state.readiness;
|
|
279
281
|
const blocked = BLOCKED_STATES.has(r.state) && r.commandId !== null;
|
|
282
|
+
const waiting = r.state === 'waiting-done' && r.commandId !== null;
|
|
280
283
|
return {
|
|
281
284
|
deliveryState: r.state,
|
|
282
285
|
queuedCommandId: blocked ? r.commandId : null,
|
|
283
286
|
undeliveredSec: blocked ? Math.max(0, Math.round((now - r.since) / 1000)) : null,
|
|
287
|
+
waitingSec: waiting ? Math.max(0, Math.round((now - r.since) / 1000)) : null,
|
|
284
288
|
};
|
|
285
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* 配った便の完了を待っている1周を記録する(★無言の周をなくす・案B-2)。
|
|
292
|
+
*
|
|
293
|
+
* 【なぜ要るか】追跡中(pending)の周は、理由を1行も書かずに return する経路が複数あった。
|
|
294
|
+
* `noteBusyEdge` は【変わり目だけ】出す設計なので、同じ状態が続く限り何も出ない。
|
|
295
|
+
* 「作業中の可能性ありスキップ」のログは pending が無い周にしか到達しない。
|
|
296
|
+
* =実機 2026-10-02 は、空の入力待ち画面を「作業中」と誤読したまま10分以上、
|
|
297
|
+
* 現場のログに1行も出なかった。**この1行があれば即座に原因が分かった。**
|
|
298
|
+
*
|
|
299
|
+
* 【★画面をどう読んだかを必ず書く】これが要。「経過」だけでは誤読に気づけない。
|
|
300
|
+
* 【★記録を埋めない】生存通知と同じしぼり(RELAY_HEARTBEAT_INTERVAL_MS=150秒)に相乗りする。
|
|
301
|
+
*
|
|
302
|
+
* @returns 実際に1行出したら true(しぼりで出さなかったら false)。
|
|
303
|
+
*/
|
|
304
|
+
export function noteWaitingDone(state, site, screen, askedCentral, logLine, now = Date.now()) {
|
|
305
|
+
const p = state.pending;
|
|
306
|
+
if (p === null)
|
|
307
|
+
return false;
|
|
308
|
+
const last = p.lastWaitLogAt ?? null;
|
|
309
|
+
if (last !== null && now - last < RELAY_HEARTBEAT_INTERVAL_MS)
|
|
310
|
+
return false;
|
|
311
|
+
p.lastWaitLogAt = now;
|
|
312
|
+
logLine(`site=${site} id=${p.id} 完了待ち(経過${Math.max(0, Math.round((now - p.sentAt) / 1000))}s` +
|
|
313
|
+
`・画面=${screen}・中央へ問い合わせ=${askedCentral ? 'した' : 'しない'})`);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 合図(耳からの「今すぐ1周」)を受けたのに取得へ進まなかった理由を1行出す(★案B-3)。
|
|
318
|
+
* ★しぼり(RELAY_HEARTBEAT_INTERVAL_MS)付き=記録を埋めない。出したら合図は消費する。
|
|
319
|
+
*
|
|
320
|
+
* @returns 実際に1行出したら true。
|
|
321
|
+
*/
|
|
322
|
+
export function noteNudgeSkipped(state, site, reason, logLine, now = Date.now()) {
|
|
323
|
+
if (state.nudgedAt === null)
|
|
324
|
+
return false;
|
|
325
|
+
state.nudgedAt = null; // 合図は1回ぶん。消費して次の合図を待つ。
|
|
326
|
+
if (state.lastNudgeLogAt !== null &&
|
|
327
|
+
now - state.lastNudgeLogAt < RELAY_HEARTBEAT_INTERVAL_MS) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
state.lastNudgeLogAt = now;
|
|
331
|
+
logLine(`site=${site} 合図を受けましたが${reason}のため、この周では指示を取得しません`);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
286
334
|
/**
|
|
287
335
|
* 管制へつながらないときの、次の再試行までの待ち時間(ms)。純粋関数(I/Oなし=単体テスト可能)。
|
|
288
336
|
*
|
|
@@ -425,6 +473,16 @@ export const CODEX_MARKERS = [
|
|
|
425
473
|
export const CODEX_BUSY_MARKERS = ['esc to interrupt', 'working ('];
|
|
426
474
|
/** Codex の選択メニューのカーソル(実測: U+203A)。★Claude の `❯`(U+276F) とは別物。 */
|
|
427
475
|
export const CODEX_MENU_CURSOR = '›';
|
|
476
|
+
/**
|
|
477
|
+
* Codex の「待機」に要求する、入力欄の行の位置(末尾から何行以内か)。
|
|
478
|
+
*
|
|
479
|
+
* 【根拠(実測)】0.160.0 の起動画面は末尾の空行を落とすと
|
|
480
|
+
* `› Ask Codex to do anything` / 空行 / `GPT-6.1-Sol default · ~/codex-probe` / `? for shortcuts`
|
|
481
|
+
* =入力欄は末尾から4行目。0.152.1 の待機画面は末尾から2〜3行目(test/relay-agent-kind.test.ts)。
|
|
482
|
+
* 余裕を見て8行に取る。★画面を覆う表示(警告・全画面のダイアログ)では入力欄が
|
|
483
|
+
* 1行も見えないので、何行に取っても弾ける=この値は「見えている待機」を取りこぼさないための幅。
|
|
484
|
+
*/
|
|
485
|
+
export const CODEX_IDLE_INPUT_WITHIN_LINES = 8;
|
|
428
486
|
/**
|
|
429
487
|
* 種類ごとの目印を返す。★関数にしてあるのは、定数の宣言順に縛られないようにするため
|
|
430
488
|
* (CLAUDE_MARKERS などはこの下で宣言される)。
|
|
@@ -434,29 +492,52 @@ export function agentProfile(kind) {
|
|
|
434
492
|
return {
|
|
435
493
|
markers: CODEX_MARKERS,
|
|
436
494
|
busyMarkers: CODEX_BUSY_MARKERS,
|
|
495
|
+
// ★Codex は回転印を見ない(起動画面の点字ロゴを作業中と誤読する=調査 67f8c51f)。
|
|
496
|
+
busyGlyphs: [],
|
|
437
497
|
cursor: CODEX_MENU_CURSOR,
|
|
438
498
|
idleRule: 'not-busy',
|
|
499
|
+
idleInputLineWithin: CODEX_IDLE_INPUT_WITHIN_LINES,
|
|
439
500
|
};
|
|
440
501
|
}
|
|
441
502
|
return {
|
|
442
503
|
markers: CLAUDE_MARKERS,
|
|
443
504
|
busyMarkers: BUSY_MARKERS,
|
|
505
|
+
// ★claude は従来どおり BUSY_GLYPHS そのまま(判定を1文字も変えない)。
|
|
506
|
+
busyGlyphs: BUSY_GLYPHS,
|
|
444
507
|
cursor: '❯',
|
|
445
508
|
idleRule: 'prompt',
|
|
509
|
+
idleInputLineWithin: null,
|
|
446
510
|
};
|
|
447
511
|
}
|
|
448
512
|
/** 種類を見て「作業中か」を判定する。claude は従来の hasBusySignal と同一。 */
|
|
449
513
|
export function hasBusySignalFor(text, kind) {
|
|
450
514
|
if (kind === 'claude')
|
|
451
515
|
return hasBusySignal(text);
|
|
516
|
+
const profile = agentProfile(kind);
|
|
452
517
|
const regionText = busyScanRegion(text);
|
|
453
518
|
const lower = regionText.toLowerCase();
|
|
454
|
-
if (
|
|
519
|
+
if (profile.busyMarkers.some((m) => lower.includes(m)))
|
|
455
520
|
return true;
|
|
456
|
-
|
|
521
|
+
// ★回転印は種類ごとの表から引く(Codex は空=見ない)。従来は BUSY_GLYPHS を共有していた。
|
|
522
|
+
if (profile.busyGlyphs.some((g) => regionText.includes(g)))
|
|
457
523
|
return true;
|
|
458
524
|
return false;
|
|
459
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* 「入力欄の行(cursor で始まる最後の行)が末尾から within 行以内に在るか」を返す純粋関数。
|
|
528
|
+
* ★枠の装飾(│ など)は剥がしてから見る。末尾の空行は数えない(tmux は下を空行で埋める)。
|
|
529
|
+
*/
|
|
530
|
+
export function hasInputLineNearBottom(text, cursor, within) {
|
|
531
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
532
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '')
|
|
533
|
+
lines.pop();
|
|
534
|
+
const from = Math.max(0, lines.length - within);
|
|
535
|
+
for (let i = lines.length - 1; i >= from; i--) {
|
|
536
|
+
if (lines[i].replace(/^[│|╎┃\s]+/, '').startsWith(cursor))
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
460
541
|
/**
|
|
461
542
|
* 画面テキストから「アイドルか」を判定する純粋関数(判定(a))。
|
|
462
543
|
* 方針:「確実な処理中シグナル(esc to interrupt / compacting / スピナーグリフ)が無く、
|
|
@@ -698,12 +779,21 @@ export function looksLikeAgent(text, kind) {
|
|
|
698
779
|
export function looksIdleFor(text, kind) {
|
|
699
780
|
if (kind === 'claude')
|
|
700
781
|
return looksIdle(text);
|
|
782
|
+
const profile = agentProfile(kind);
|
|
701
783
|
if (hasBusySignalFor(text, kind))
|
|
702
784
|
return false;
|
|
703
785
|
if (hasChoiceMenuFor(text, kind))
|
|
704
786
|
return false;
|
|
705
787
|
// ★その窓が Codex だと分かるときだけ「待機」と言い切る。分からない窓は待機にしない。
|
|
706
|
-
|
|
788
|
+
if (!looksLikeAgent(text, kind))
|
|
789
|
+
return false;
|
|
790
|
+
// ★入力欄が末尾付近に見えることを必須にする(案C-2・AgentProfile.idleInputLineWithin 参照)。
|
|
791
|
+
// 画面を覆う表示(0.160.0 の警告の表示など)は入力欄を隠すので、ここで弾ける。
|
|
792
|
+
if (profile.idleInputLineWithin !== null &&
|
|
793
|
+
!hasInputLineNearBottom(text, profile.cursor, profile.idleInputLineWithin)) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
return true;
|
|
707
797
|
}
|
|
708
798
|
/**
|
|
709
799
|
* 画面テキストに「完了マーカー DONE:{site} が単独で出力されているか」を判定する純粋関数。
|
|
@@ -2894,6 +2984,7 @@ export function startKeepalive(state, pending, now) {
|
|
|
2894
2984
|
anchor: pending.anchor,
|
|
2895
2985
|
startedAt: now,
|
|
2896
2986
|
lastHeartbeatAt: null,
|
|
2987
|
+
lastCentralProbeAt: null,
|
|
2897
2988
|
screenDoneUsable: true,
|
|
2898
2989
|
};
|
|
2899
2990
|
return true;
|
|
@@ -2916,8 +3007,12 @@ export function decideKeepalive(input) {
|
|
|
2916
3007
|
return 'wait';
|
|
2917
3008
|
if (input.doneSeen)
|
|
2918
3009
|
return 'done';
|
|
2919
|
-
if (input.alive)
|
|
3010
|
+
if (input.alive) {
|
|
3011
|
+
// ★alive が続いていても、しぼりが空いたら中央へ答え合わせする(画面の誤読から必ず抜ける道)。
|
|
3012
|
+
if (input.centralProbeOk === true)
|
|
3013
|
+
return 'alive-probe';
|
|
2920
3014
|
return input.throttleOk ? 'heartbeat' : 'wait';
|
|
3015
|
+
}
|
|
2921
3016
|
if (input.idle)
|
|
2922
3017
|
return 'probe';
|
|
2923
3018
|
return 'wait';
|
|
@@ -2970,9 +3065,34 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
2970
3065
|
alive: cap !== null && (kaBusySignal || kaMenu),
|
|
2971
3066
|
idle: cap !== null && looksIdleFor(cap, kindOf(cfg)),
|
|
2972
3067
|
throttleOk: ka.lastHeartbeatAt === null || now - ka.lastHeartbeatAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
3068
|
+
// ★中央への答え合わせのしぼり。生存通知と同じ150秒(RELAY_HEARTBEAT_INTERVAL_MS)。
|
|
3069
|
+
centralProbeOk: ka.lastCentralProbeAt === null || now - ka.lastCentralProbeAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
2973
3070
|
});
|
|
2974
3071
|
if (action === 'wait')
|
|
2975
3072
|
return;
|
|
3073
|
+
if (action === 'alive-probe') {
|
|
3074
|
+
// ★画面は「作業中」に見えるが、中央にも必ず聞く(画面の誤読から抜ける唯一の道・案A-2)。
|
|
3075
|
+
ka.lastCentralProbeAt = now;
|
|
3076
|
+
const alive = await getStatus(cfg, ka.id);
|
|
3077
|
+
if (isCentralFinished(alive)) {
|
|
3078
|
+
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 中央で終端(status=${alive})を確認` +
|
|
3079
|
+
'(画面は作業中に見えたが中央の真実で畳む)');
|
|
3080
|
+
noteBusyEdge(state, cfg.site, ka.id, false, logLine);
|
|
3081
|
+
state.keepalive = null;
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
// 終端でない=従来の 'heartbeat' と同じ扱い(手は挙げたまま生存通知)。しぼりは尊重する。
|
|
3085
|
+
const raiseAlive = shouldRaiseBusy(kaBusySignal, kaMenu);
|
|
3086
|
+
noteBusyEdge(state, cfg.site, ka.id, raiseAlive, logLine);
|
|
3087
|
+
if (ka.lastHeartbeatAt === null || now - ka.lastHeartbeatAt >= RELAY_HEARTBEAT_INTERVAL_MS) {
|
|
3088
|
+
ka.lastHeartbeatAt = now;
|
|
3089
|
+
logLine(`site=${cfg.site} id=${ka.id} 見張り継続: 職人は生きています→生存通知` +
|
|
3090
|
+
`(中央へ問い合わせ=した・status=${alive ?? '不明'}` +
|
|
3091
|
+
`・経過${Math.round((now - ka.startedAt) / 1000)}s)`);
|
|
3092
|
+
await heartbeat(cfg, ka.id, raiseAlive);
|
|
3093
|
+
}
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
2976
3096
|
if (action === 'heartbeat') {
|
|
2977
3097
|
ka.lastHeartbeatAt = now;
|
|
2978
3098
|
const raise = shouldRaiseBusy(kaBusySignal, kaMenu);
|
|
@@ -3102,8 +3222,11 @@ export async function restoreOrphans(cfg, state) {
|
|
|
3102
3222
|
log(`site=${cfg.site} id=${pick.id} を sent孤児から pending へ復元(★配達はしない・DONE待ちと生存通知だけ)`);
|
|
3103
3223
|
}
|
|
3104
3224
|
export async function relayTick(cfg, state, hooks) {
|
|
3105
|
-
if (state.running)
|
|
3225
|
+
if (state.running) {
|
|
3226
|
+
// ★合図を受けていたなら、空振りした理由を1行残す(しぼり付き・案B-3)。
|
|
3227
|
+
noteNudgeSkipped(state, cfg.site, '前の周がまだ走っている', log);
|
|
3106
3228
|
return; // 前回の tick が走行中ならスキップ
|
|
3229
|
+
}
|
|
3107
3230
|
state.running = true;
|
|
3108
3231
|
// 承認待ちの観測値は毎tick「出ていない」から始める(下でプロンプトを検知したときだけ true にする)。
|
|
3109
3232
|
// ここで false に戻しておくと、プロンプトが消えた/pending が無い/早期 return のどの経路でも、
|
|
@@ -3135,9 +3258,18 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3135
3258
|
await keepaliveTick(cfg, state);
|
|
3136
3259
|
// ── 完了マーカー検出(pending があれば最優先・停止中でも継続) ──
|
|
3137
3260
|
if (state.pending) {
|
|
3138
|
-
|
|
3139
|
-
|
|
3261
|
+
// ★この周の結末を「配った便の完了待ち」として記録する(案B-1)。ここで1回呼べば、
|
|
3262
|
+
// 以下のどの return 経路を通っても管制の札が 'delivered' のまま固まらない。
|
|
3263
|
+
// 同じ状態・同じ指示の間 since は動かないので、待っている長さがそのまま測れる。
|
|
3264
|
+
noteReadiness(state, 'waiting-done', state.pending.id);
|
|
3265
|
+
// ★合図が来ていたなら、取得へ進まない理由を1行残す(しぼり付き・案B-3)。
|
|
3266
|
+
noteNudgeSkipped(state, cfg.site, `配った便(id=${state.pending.id})の完了を待っている`, log);
|
|
3267
|
+
const cap = (hooks?.capture ?? tmuxCapture)(cfg.site);
|
|
3268
|
+
if (cap === null) {
|
|
3269
|
+
// ★無言で返さない(案B-2)。画面が読めない周も、しぼり付きで1行残す。
|
|
3270
|
+
noteWaitingDone(state, cfg.site, '読めない', false, log);
|
|
3140
3271
|
return; // tmux未取得: 次回再確認
|
|
3272
|
+
}
|
|
3141
3273
|
// ── 完了検出の走査範囲を広げてよいかの観測(★状態更新には使わない・ADR-023) ──
|
|
3142
3274
|
// アンカーを一度でも画面で見たら、以後アンカーが流れて消えても全画面から DONE を拾ってよい
|
|
3143
3275
|
// (ADR-020 の根拠)。★ここで観測するのは「どこまで見てよいか」だけで、配達の成否や
|
|
@@ -3211,9 +3343,25 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3211
3343
|
// 【副シグナル温存=無損失縮退】中央 status が読めない(null=エンドポイント未実装/不達)ときだけ、
|
|
3212
3344
|
// 従来の detectExternalDone(別idが配れる=終端の確証)へフォールバックする。中央側 status API が
|
|
3213
3345
|
// 入るまではこの副シグナルが従来どおり効き、入った後は主シグナル(status)が確実に固着を断つ。
|
|
3214
|
-
//
|
|
3346
|
+
// 【★画面の判定から切り離す(根治・案A-2・調査 67f8c51f)】
|
|
3347
|
+
// 従来の条件は「印が付いている+**画面がアイドルに見える**」だった。ところが画面の目印は
|
|
3348
|
+
// 職人側の版上げで必ず壊れる。実機 2026-10-02 は Codex 0.160.0 の起動画面(点字のロゴ)を
|
|
3349
|
+
// 「作業中」と誤読し、idle が永久に false になった=**中央の真実を知る唯一の道が閉じた**。
|
|
3350
|
+
// 中央でとうに done の便を15分抱え、そのあと見張りへ渡って最長3時間ふさがった。
|
|
3351
|
+
// =画面が外れたら逃げ道も消える、という構造そのものが穴。
|
|
3352
|
+
// そこで条件を「印が付いている」だけにし、代わりに時計でしぼる:
|
|
3353
|
+
// ・アイドルに見える周 … 従来どおり毎tick叩く(★いちばん速い道は1バイトも遅くしない)
|
|
3354
|
+
// ・それ以外の周 … 150秒(RELAY_HEARTBEAT_INTERVAL_MS・生存通知と同じ時計)に1回
|
|
3355
|
+
// =HTTP の増分は1指示あたり最大で150秒に1回。画面の読み取りが外れても、中央で終わった便は
|
|
3356
|
+
// 必ず150秒以内の周で追跡解除され、次の便が配られる。
|
|
3215
3357
|
const idle = looksIdleFor(cap, kindOf(cfg));
|
|
3216
|
-
|
|
3358
|
+
const probeBase = state.pending.lastStatusProbeAt ?? state.pending.sentAt;
|
|
3359
|
+
const probeDue = Date.now() - probeBase >= RELAY_HEARTBEAT_INTERVAL_MS;
|
|
3360
|
+
const askedCentral = state.pending.sentMarked && (idle || probeDue);
|
|
3361
|
+
// ★画面をどう読んだかを必ず残す(案B-2)。これが1行あれば今回の誤読は即座に分かった。
|
|
3362
|
+
noteWaitingDone(state, cfg.site, idle ? '待機' : '作業中', askedCentral, log);
|
|
3363
|
+
if (askedCentral) {
|
|
3364
|
+
state.pending.lastStatusProbeAt = Date.now();
|
|
3217
3365
|
const centralStatus = await fetchCommandStatus(cfg, state.pending.id);
|
|
3218
3366
|
// ★本文未着(instruction_missing)も追跡をやめる(便 7d810bbb)。管制は占有を外して人の判断待ちに
|
|
3219
3367
|
// 出している=ここで待ち続けると次の便が拾えない。★配り直さない(人が決める)。
|
|
@@ -3226,7 +3374,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3226
3374
|
}
|
|
3227
3375
|
// 中央 status が未知(null)のときのみ、従来の副シグナル(別idが配信可能=終端の確証)で救う。
|
|
3228
3376
|
// 中央が sent/queued 等の「生存中」を明示的に返した場合は解除しない(中央の真実を尊重)。
|
|
3229
|
-
|
|
3377
|
+
// ★副シグナルは「アイドル」を根拠の一部にしている(detectExternalDone)。だから画面が
|
|
3378
|
+
// アイドルに見える周だけ叩く=従来と同じ条件・同じ結論。無駄な取得も増やさない。
|
|
3379
|
+
if (centralStatus === null && idle) {
|
|
3230
3380
|
const next = await fetchCommand(cfg);
|
|
3231
3381
|
const nextId = next?.id ?? null;
|
|
3232
3382
|
if (detectExternalDone({
|
|
@@ -3498,7 +3648,15 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3498
3648
|
// 新しい指示の生存ハートビートは先頭から数え直す(前指示のスロットルを引き継がない)。
|
|
3499
3649
|
state.lastHeartbeatAt = null;
|
|
3500
3650
|
noteReadiness(state, 'delivered', cmd.id);
|
|
3501
|
-
|
|
3651
|
+
// ★記録の文面を事実に合わせる(案C-4・調査 67f8c51f)。
|
|
3652
|
+
// 「貼り付けが入力欄に入ったか」を確かめているのは、受領確認を通る職人(claude)だけ。
|
|
3653
|
+
// それ以外(Codex)は tmux へ送れたかしか見ていないので、画面を覆う表示(起動画面・警告の
|
|
3654
|
+
// 表示)へ打ち込んで本文が捨てられても「配達しました」と書いていた=人が記録を信じて誤診した
|
|
3655
|
+
// (実機 2026-10-02)。確認が入る(要判断3)までの間、書く言葉を実際に確かめた範囲に合わせる。
|
|
3656
|
+
// ★claude の文面は1文字も変えない。
|
|
3657
|
+
log(useAck
|
|
3658
|
+
? `site=${cfg.site} id=${cmd.id} ${landed ? '配達しました' : '配達を試みましたが貼り付けが入らず、何も送信していません'}`
|
|
3659
|
+
: `site=${cfg.site} id=${cmd.id} 送信しました(入力欄に入ったかは未確認)`);
|
|
3502
3660
|
// ★配った時点で管制へ印を書く(便 7d810bbb)。Claude の職人は「配達中(受領確認待ち)」、
|
|
3503
3661
|
// それ以外は今までどおり「配達済み(sent)」。書けなければ次tick以降で書けるまでやり直す
|
|
3504
3662
|
// (markSentAtCentral)。★書けなくても再配達はしない。
|