@tyhld/conductor 0.12.106 → 0.12.107
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/relay.js +224 -5
- package/package.json +1 -1
package/dist/relay.js
CHANGED
|
@@ -1959,6 +1959,73 @@ export function countStandaloneDone(text, site) {
|
|
|
1959
1959
|
n += 1;
|
|
1960
1960
|
return n;
|
|
1961
1961
|
}
|
|
1962
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1963
|
+
// 終わりの印が【行の中】に描かれても拾う(★Codex だけ・便 0029ef4d)
|
|
1964
|
+
//
|
|
1965
|
+
// 【何が起きたか(実物 2026-10-05 夜・便 cd7d9ea8・配り役 0.12.101)】
|
|
1966
|
+
// Codex は 23:33 に返事を終え、入力待ち(`› Ask Codex to do anything`)に戻っていた。
|
|
1967
|
+
// ところが返事の本文が表(マークダウンの table)で終わっていたため、Codex の画面は
|
|
1968
|
+
// そのあとの2行まで【表の行として】描いた:
|
|
1969
|
+
// 型 =====原文ここまで===== そのまま使える物 …
|
|
1970
|
+
// 型 DONE:codex-probe
|
|
1971
|
+
// 既存の判定(isStandaloneDoneLine)は「印と装飾を除いて何も残らない行」だけを完了とみなすので、
|
|
1972
|
+
// 行の頭に残った `型` のせいで永久に成立しない。配り役の記録には 178 秒めから
|
|
1973
|
+
// 「画面=待機」と出ていたのに、完了は 15 分の上限まで拾えず、報告本文も載らなかった。
|
|
1974
|
+
//
|
|
1975
|
+
// 【直し方の筋】印の【描かれ方】だけに頼らない。「この行は印で終わっている」+「職人は入力待ちに
|
|
1976
|
+
// 戻っている」の2つで読む。印の【前】に何が付いていてもよく、印の【後ろ】に字が残っていたら拾わない。
|
|
1977
|
+
//
|
|
1978
|
+
// 【★指示文のこだま(画面に出る指示の写し)を終わりと取り違えない】守りは3重で、1つも緩めない:
|
|
1979
|
+
// ① 印の【後ろ】に字が残る行は拾わない … 配達文の「最終行: DONE:{現場} とだけ出力」は
|
|
1980
|
+
// 印の後ろに字が続くので、この1つ目で落ちる。
|
|
1981
|
+
// ② 入力欄に写った行(行頭が Codex のカーソル `›`)・アンカーを含む行は拾わない。
|
|
1982
|
+
// 貼った本文の写しは必ずこの形で描かれる(実測)。
|
|
1983
|
+
// ③ 床(配った時点の数)より増えたときだけ完了とみなす … 端末の折り返しがたまたま印の直後で
|
|
1984
|
+
// 切れて①②をすり抜けても、配った直後の周で床に取り込まれるので完了にならない。
|
|
1985
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1986
|
+
/**
|
|
1987
|
+
* 印の【後ろ】に残っていても「字が無い」とみなす飾り。
|
|
1988
|
+
* 既存の単独行の判定(isStandaloneDoneLine)の飾りに、表・区切りの罫線と Codex のカーソルを足す。
|
|
1989
|
+
* ★既存の判定そのものは1文字も変えない(Claude の道を動かさないため)。
|
|
1990
|
+
*/
|
|
1991
|
+
const TRAILING_DONE_DECOR_RE = /[│|>›⏺●○•*·─━┃┆┊╎—–\s]/g;
|
|
1992
|
+
/**
|
|
1993
|
+
* 1行が「`DONE:{site}` で終わっている行」か(★印の前に何が付いていてもよい)。
|
|
1994
|
+
* - 印より後ろに、飾りと空白以外が1字も残っていないこと。
|
|
1995
|
+
* - 行頭が Codex のカーソル(`›`)の行=入力欄に写った指示文は拾わない。
|
|
1996
|
+
* - アンカー(`CONDUCTOR_JOB`)を含む行=貼った本文の写しは拾わない。
|
|
1997
|
+
*/
|
|
1998
|
+
export function isTrailingDoneLine(line, marker) {
|
|
1999
|
+
const at = line.lastIndexOf(marker);
|
|
2000
|
+
if (at < 0)
|
|
2001
|
+
return false;
|
|
2002
|
+
if (line.slice(at + marker.length).replace(TRAILING_DONE_DECOR_RE, '').length > 0)
|
|
2003
|
+
return false;
|
|
2004
|
+
const head = line.trim();
|
|
2005
|
+
if (head.startsWith(CODEX_MENU_CURSOR))
|
|
2006
|
+
return false;
|
|
2007
|
+
if (line.includes(SEND_ANCHOR_PREFIX))
|
|
2008
|
+
return false;
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
/** テキスト中に「`DONE:{site}` で終わる行」が1つでもあるか。 */
|
|
2012
|
+
export function hasTrailingDone(text, site) {
|
|
2013
|
+
const marker = `DONE:${site}`;
|
|
2014
|
+
return text
|
|
2015
|
+
.replace(/\r/g, '')
|
|
2016
|
+
.split('\n')
|
|
2017
|
+
.some((line) => isTrailingDoneLine(line, marker));
|
|
2018
|
+
}
|
|
2019
|
+
/** テキスト中の「`DONE:{site}` で終わる行」の数(★床の基準に使う)。 */
|
|
2020
|
+
export function countTrailingDone(text, site) {
|
|
2021
|
+
const marker = `DONE:${site}`;
|
|
2022
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
2023
|
+
let n = 0;
|
|
2024
|
+
for (const line of lines)
|
|
2025
|
+
if (isTrailingDoneLine(line, marker))
|
|
2026
|
+
n += 1;
|
|
2027
|
+
return n;
|
|
2028
|
+
}
|
|
1962
2029
|
/**
|
|
1963
2030
|
* 追跡中(pending)の便について、画面から完了の合図を読む(★I/Oなし=単体テスト可能)。
|
|
1964
2031
|
*
|
|
@@ -1984,6 +2051,25 @@ export function scanDoneForPending(cap, site, anchor, opts) {
|
|
|
1984
2051
|
return { done: true, floor: opts.doneFloor };
|
|
1985
2052
|
return { done: false, floor: Math.min(opts.doneFloor, seen) };
|
|
1986
2053
|
}
|
|
2054
|
+
/**
|
|
2055
|
+
* 「職人は入力待ちに戻っている」ときだけ、行の中に描かれた終わりの印を読む(★I/Oなし)。
|
|
2056
|
+
*
|
|
2057
|
+
* ★上の scanDoneForPending(今までの道)は1バイトも変えない。これは【それが空振りしたときだけ】
|
|
2058
|
+
* 足す2本目の道で、Codex の便で `idle:true` の周にしか効かない。
|
|
2059
|
+
* - `idle:false`(まだ手を動かしている・Claude の現場)… 何も読まない・床も動かさない。
|
|
2060
|
+
* - 床が在る … 配った時点の数より増えたときだけ完了。増えていなければ観測した数まで床を下げる。
|
|
2061
|
+
* - 床が無い(孤児の復元など)… 今までの単独行の判定と同じ危険度で、1本でも在れば完了。
|
|
2062
|
+
*/
|
|
2063
|
+
export function scanTrailingDoneForPending(cap, site, opts) {
|
|
2064
|
+
if (!opts.idle)
|
|
2065
|
+
return { done: false, floor: opts.floor };
|
|
2066
|
+
if (opts.floor === null)
|
|
2067
|
+
return { done: hasTrailingDone(cap, site), floor: null };
|
|
2068
|
+
const seen = countTrailingDone(cap, site);
|
|
2069
|
+
if (seen > opts.floor)
|
|
2070
|
+
return { done: true, floor: opts.floor };
|
|
2071
|
+
return { done: false, floor: Math.min(opts.floor, seen) };
|
|
2072
|
+
}
|
|
1987
2073
|
/**
|
|
1988
2074
|
* アンカー基準の完了判定(根治の中核)。
|
|
1989
2075
|
* 「アンカーより下の領域」に DONE:{site} 単独行があれば完了とみなす。
|
|
@@ -4289,6 +4375,71 @@ export async function sendHeartbeat(cfg, id, busy = false, liveness) {
|
|
|
4289
4375
|
log(`site=${cfg.site} id=${id} heartbeat -> 失敗: ${err instanceof Error ? err.message : String(err)}(縮退で安全)`);
|
|
4290
4376
|
}
|
|
4291
4377
|
}
|
|
4378
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4379
|
+
// 「職人は入力待ちに戻っているが、終わりの印が見つからない」を管制へ知らせる(便 0029ef4d)
|
|
4380
|
+
//
|
|
4381
|
+
// 【なぜ要るか(実物 2026-10-05 夜・便 cd7d9ea8)】配り役の記録には 178 秒めから
|
|
4382
|
+
// 「完了待ち(画面=待機)」と出ていた。=配り役は「職人の手は止まっている」と分かっていたのに、
|
|
4383
|
+
// その事実は手元の記録にしか残らず、管制には何も届かなかった。管制の画面では
|
|
4384
|
+
// 「返事待ち(15分)→ 確認しています(30分)→ 回収必要」と時間だけが進み、人が Codex の画面を
|
|
4385
|
+
// 見るまで誰も気づけなかった。
|
|
4386
|
+
//
|
|
4387
|
+
// 【どの口を使うか(★管制のリポは触らない)】既にある出来事の口
|
|
4388
|
+
// `POST /api/conductor/commands/{id}/events`(画面なし方式の職人が進捗を送っている口)へ、
|
|
4389
|
+
// 既にある種類 `progress` で1行送るだけ。★便の状態は1文字も書かない(完了にしない・
|
|
4390
|
+
// `lastSeenAt` も動かさない=管制の時間切れの働きを遅らせない)。
|
|
4391
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
4392
|
+
/** 「入力待ちなのに印が無い」を管制へ知らせるまでの待ち(★短すぎると普通の間(ま)で鳴る)。 */
|
|
4393
|
+
export const RELAY_IDLE_NO_DONE_NOTICE_MS = 5 * 60 * 1000;
|
|
4394
|
+
/**
|
|
4395
|
+
* この周で「入力待ちなのに終わりの印が無い」を管制へ知らせるかを決める純粋関数。
|
|
4396
|
+
*
|
|
4397
|
+
* ★1便に1回だけ(told)。★Codex の現場だけ(Claude は画面なし方式=終わりを実プロセスで掴める)。
|
|
4398
|
+
* ★印が見つかった周・まだ手を動かしている周では鳴らさない(idleNoDoneSince が null に戻る)。
|
|
4399
|
+
*/
|
|
4400
|
+
export function decideIdleNoDoneNotice(args) {
|
|
4401
|
+
if (args.kind !== 'codex')
|
|
4402
|
+
return false;
|
|
4403
|
+
if (args.told)
|
|
4404
|
+
return false;
|
|
4405
|
+
if (!args.idle || args.doneSeen)
|
|
4406
|
+
return false;
|
|
4407
|
+
if (args.idleNoDoneSince === null)
|
|
4408
|
+
return false;
|
|
4409
|
+
return args.now - args.idleNoDoneSince >= RELAY_IDLE_NO_DONE_NOTICE_MS;
|
|
4410
|
+
}
|
|
4411
|
+
/** 管制へ知らせる1行の本文(★純粋関数・秘密は1文字も入れない)。 */
|
|
4412
|
+
export function idleNoDoneNoticeText(site, idleSeconds) {
|
|
4413
|
+
return (`職人(Codex)は入力待ちに戻っていますが、終わりの印(DONE:${site})が画面に見つかりません` +
|
|
4414
|
+
`(入力待ちになってから約${Math.max(0, Math.round(idleSeconds))}秒)。` +
|
|
4415
|
+
'返事を書き終えているのに印の書き方が違う/印が表の行として描かれた、のどちらかの可能性があります。');
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* 管制の出来事の口へ1行送る(★最善努力。失敗は握りつぶしてログだけ)。
|
|
4419
|
+
* 便の状態は書かない・報告も送らない=この呼び出しで便が完了になることは無い。
|
|
4420
|
+
*/
|
|
4421
|
+
export async function sendCommandNotice(cfg, commandId, text, opts = {}) {
|
|
4422
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
4423
|
+
const logLine = opts.logLine ?? log;
|
|
4424
|
+
const at = new Date(opts.now?.() ?? Date.now()).toISOString();
|
|
4425
|
+
try {
|
|
4426
|
+
const res = await doFetch(`${cfg.url}/api/conductor/commands/${encodeURIComponent(commandId)}/events`, {
|
|
4427
|
+
method: 'POST',
|
|
4428
|
+
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
4429
|
+
body: JSON.stringify({ events: [{ type: 'progress', text, at, attempt: 1 }] }),
|
|
4430
|
+
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
|
4431
|
+
});
|
|
4432
|
+
if (res.ok)
|
|
4433
|
+
return true;
|
|
4434
|
+
logLine(`site=${cfg.site} id=${commandId} 気づきの知らせ -> HTTP ${res.status}(送れなくても作業は止めません)`);
|
|
4435
|
+
return false;
|
|
4436
|
+
}
|
|
4437
|
+
catch (err) {
|
|
4438
|
+
logLine(`site=${cfg.site} id=${commandId} 気づきの知らせ -> 失敗: ` +
|
|
4439
|
+
`${err instanceof Error ? err.message : String(err)}(送れなくても作業は止めません)`);
|
|
4440
|
+
return false;
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4292
4443
|
/** 便の終わりの関門の種類(控えの数え方の見出し。★`finish_gate.py` の GATE_KINDS と同じ語)。 */
|
|
4293
4444
|
export const FINISH_GATE_KINDS = ['report', 'pr', 'background'];
|
|
4294
4445
|
/** 届けられなかった「終了の事実」の置き場。 */
|
|
@@ -4520,6 +4671,8 @@ export function startKeepalive(state, pending, now) {
|
|
|
4520
4671
|
lastHeartbeatAt: null,
|
|
4521
4672
|
lastCentralProbeAt: null,
|
|
4522
4673
|
screenDoneUsable: true,
|
|
4674
|
+
// ★2本目の物差しの床を引き継ぐ(★引き継がないと、履歴に残る前の便の印を拾いかねない)。
|
|
4675
|
+
...(pending.doneFloorLoose != null ? { doneFloorLoose: pending.doneFloorLoose } : {}),
|
|
4523
4676
|
};
|
|
4524
4677
|
return true;
|
|
4525
4678
|
}
|
|
@@ -4637,12 +4790,26 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
4637
4790
|
// 生存通知の可否(alive)は従来どおり busy ∪ メニュー=1文字も変えない。
|
|
4638
4791
|
const kaBusySignal = cap !== null && hasBusySignalFor(cap, kindOf(cfg));
|
|
4639
4792
|
const kaMenu = cap !== null && hasChoiceMenuFor(cap, kindOf(cfg));
|
|
4793
|
+
const kaIdle = cap !== null && looksIdleFor(cap, kindOf(cfg));
|
|
4794
|
+
// ★見張りでも、今までの道(単独行)が空振りしたら2本目の道を見る(便 0029ef4d)。
|
|
4795
|
+
// Codex の便で入力待ちの周だけ・引き継いだ床より増えたときだけ=守りは1つも緩めない。
|
|
4796
|
+
const kaTrailingScan = ka.screenDoneUsable && below !== null && kindOf(cfg) === 'codex'
|
|
4797
|
+
? scanTrailingDoneForPending(below, ka.site, {
|
|
4798
|
+
idle: kaIdle,
|
|
4799
|
+
floor: ka.doneFloorLoose ?? null,
|
|
4800
|
+
})
|
|
4801
|
+
: null;
|
|
4802
|
+
// ★床は下がるだけ(古い印の行が履歴から流れたぶんだけ下げる=あとで出た合図を取りこぼさない)。
|
|
4803
|
+
if (kaTrailingScan !== null && ka.doneFloorLoose != null && kaTrailingScan.floor !== ka.doneFloorLoose) {
|
|
4804
|
+
ka.doneFloorLoose = kaTrailingScan.floor;
|
|
4805
|
+
}
|
|
4806
|
+
const kaTrailingDone = kaTrailingScan?.done === true;
|
|
4640
4807
|
const action = decideKeepalive({
|
|
4641
4808
|
expired,
|
|
4642
4809
|
capOk: cap !== null,
|
|
4643
|
-
doneSeen: ka.screenDoneUsable && below !== null && hasStandaloneDone(below, ka.site),
|
|
4810
|
+
doneSeen: (ka.screenDoneUsable && below !== null && hasStandaloneDone(below, ka.site)) || kaTrailingDone,
|
|
4644
4811
|
alive: cap !== null && (kaBusySignal || kaMenu),
|
|
4645
|
-
idle:
|
|
4812
|
+
idle: kaIdle,
|
|
4646
4813
|
throttleOk: ka.lastHeartbeatAt === null || now - ka.lastHeartbeatAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
4647
4814
|
// ★中央への答え合わせのしぼり。生存通知と同じ150秒(RELAY_HEARTBEAT_INTERVAL_MS)。
|
|
4648
4815
|
centralProbeOk: ka.lastCentralProbeAt === null || now - ka.lastCentralProbeAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
@@ -4702,6 +4869,8 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
4702
4869
|
if (report !== null)
|
|
4703
4870
|
markReportProcessed(ka.id); // ★消さずに処理済みへ(二度送らない)
|
|
4704
4871
|
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 画面のDONEを検出し完了を報告` +
|
|
4872
|
+
// ★どちらの物差しで拾ったかを残す(便 0029ef4d・あとで原因を追えるように)。
|
|
4873
|
+
`${kaTrailingDone ? '(印は行の中に描かれていました)' : ''}` +
|
|
4705
4874
|
`${report != null ? '(報告添付)' : kaDelivery.phase === 'none' ? '' : `(${kaDelivery.note})`}`);
|
|
4706
4875
|
// ★終わったら必ず手を下ろす(挙げっぱなしにすると現場が長くふさがる)。
|
|
4707
4876
|
noteBusyEdge(state, cfg.site, ka.id, false, logLine);
|
|
@@ -4979,6 +5148,8 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
4979
5148
|
if (state.pending.doneFloor != null && state.pending.doneFloorSettled !== true) {
|
|
4980
5149
|
// 配達のエコーを含んだ数も床に取り込む(PendingEntry.doneFloorSettled の説明)。
|
|
4981
5150
|
state.pending.doneFloor = Math.max(state.pending.doneFloor, countStandaloneDone(cap, cfg.site));
|
|
5151
|
+
// ★2本目の物差し(行の中に描かれた印)の床も、同じ周に決め終える(PendingEntry.doneFloorLoose)。
|
|
5152
|
+
state.pending.doneFloorLoose = Math.max(state.pending.doneFloorLoose ?? 0, countTrailingDone(cap, cfg.site));
|
|
4982
5153
|
state.pending.doneFloorSettled = true;
|
|
4983
5154
|
}
|
|
4984
5155
|
const scan = scanDoneForPending(cap, cfg.site, state.pending.anchor, {
|
|
@@ -4988,7 +5159,48 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
4988
5159
|
if (state.pending.doneFloor != null && scan.floor !== state.pending.doneFloor) {
|
|
4989
5160
|
state.pending.doneFloor = scan.floor;
|
|
4990
5161
|
}
|
|
4991
|
-
|
|
5162
|
+
// ★今までの道が空振りしたときだけ、2本目の道を見る(便 0029ef4d)。
|
|
5163
|
+
// 「職人は入力待ちに戻っている」+「この行は印で終わっている」の2つで読むので、
|
|
5164
|
+
// 印が表の行として描かれても(行の頭に別の字が付いても)拾える。
|
|
5165
|
+
// ★Codex の便で入力待ちの周にしか効かない=Claude の現場は1バイトも変わらない。
|
|
5166
|
+
const idle = looksIdleFor(cap, kindOf(cfg));
|
|
5167
|
+
const looseScan = scanTrailingDoneForPending(cap, cfg.site, {
|
|
5168
|
+
idle: kindOf(cfg) === 'codex' && idle && !scan.done,
|
|
5169
|
+
floor: state.pending.doneFloorLoose ?? null,
|
|
5170
|
+
});
|
|
5171
|
+
if (state.pending.doneFloorLoose != null && looseScan.floor !== state.pending.doneFloorLoose) {
|
|
5172
|
+
state.pending.doneFloorLoose = looseScan.floor;
|
|
5173
|
+
}
|
|
5174
|
+
if (looseScan.done && !scan.done) {
|
|
5175
|
+
log(`site=${cfg.site} id=${state.pending.id} 終わりの印を【行の中】で見つけました` +
|
|
5176
|
+
'(職人は入力待ちに戻っています・印が表の行として描かれた形)');
|
|
5177
|
+
}
|
|
5178
|
+
// ── 「入力待ちなのに印が見つからない」を管制へ1回だけ知らせる(便 0029ef4d) ──
|
|
5179
|
+
// ★便の状態は書かない(完了にしない)。知らせるだけ。
|
|
5180
|
+
const doneSeenNow = scan.done || looseScan.done;
|
|
5181
|
+
if (!idle || doneSeenNow) {
|
|
5182
|
+
state.pending.idleNoDoneSince = null;
|
|
5183
|
+
}
|
|
5184
|
+
else if (state.pending.idleNoDoneSince == null) {
|
|
5185
|
+
state.pending.idleNoDoneSince = Date.now();
|
|
5186
|
+
}
|
|
5187
|
+
if (decideIdleNoDoneNotice({
|
|
5188
|
+
kind: kindOf(cfg),
|
|
5189
|
+
idle,
|
|
5190
|
+
doneSeen: doneSeenNow,
|
|
5191
|
+
idleNoDoneSince: state.pending.idleNoDoneSince ?? null,
|
|
5192
|
+
now: Date.now(),
|
|
5193
|
+
told: state.pending.idleNoDoneTold === true,
|
|
5194
|
+
})) {
|
|
5195
|
+
state.pending.idleNoDoneTold = true;
|
|
5196
|
+
const idleSec = (Date.now() - (state.pending.idleNoDoneSince ?? Date.now())) / 1000;
|
|
5197
|
+
const text = idleNoDoneNoticeText(cfg.site, idleSec);
|
|
5198
|
+
log(`WARN site=${cfg.site} id=${state.pending.id} ${text}`);
|
|
5199
|
+
await sendCommandNotice(cfg, state.pending.id, text, { logLine: log });
|
|
5200
|
+
if (state.pending === null)
|
|
5201
|
+
return; // 念のため(送信中に追跡が畳まれた場合)
|
|
5202
|
+
}
|
|
5203
|
+
if (doneSeenNow) {
|
|
4992
5204
|
// ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
|
|
4993
5205
|
// 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
4994
5206
|
// ★「届いた」は形まで見る(便 461be339)。整っていなければ理由も一緒に届ける。
|
|
@@ -5035,7 +5247,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
5035
5247
|
// ・それ以外の周 … 150秒(RELAY_HEARTBEAT_INTERVAL_MS・生存通知と同じ時計)に1回
|
|
5036
5248
|
// =HTTP の増分は1指示あたり最大で150秒に1回。画面の読み取りが外れても、中央で終わった便は
|
|
5037
5249
|
// 必ず150秒以内の周で追跡解除され、次の便が配られる。
|
|
5038
|
-
|
|
5250
|
+
// ★画面の待機の読み(idle)は、上の完了検出の手前で1回だけ測って使い回す(値は同じ・純粋関数)。
|
|
5039
5251
|
const probeBase = state.pending.lastStatusProbeAt ?? state.pending.sentAt;
|
|
5040
5252
|
const probeDue = Date.now() - probeBase >= RELAY_HEARTBEAT_INTERVAL_MS;
|
|
5041
5253
|
const askedCentral = state.pending.sentMarked && (idle || probeDue);
|
|
@@ -5396,7 +5608,14 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
5396
5608
|
delivered: true,
|
|
5397
5609
|
sentMarked: false,
|
|
5398
5610
|
screenDoneWide: codexFloor !== null,
|
|
5399
|
-
...(codexFloor !== null
|
|
5611
|
+
...(codexFloor !== null
|
|
5612
|
+
? {
|
|
5613
|
+
doneFloor: codexFloor,
|
|
5614
|
+
// ★2本目の物差し(行の中に描かれた印)の床も、同じ画面から数えて覚える(便 0029ef4d)。
|
|
5615
|
+
doneFloorLoose: countTrailingDone(guardCap, cfg.site),
|
|
5616
|
+
doneFloorSettled: false,
|
|
5617
|
+
}
|
|
5618
|
+
: {}),
|
|
5400
5619
|
...(useAck ? { markAs: 'delivering' } : {}),
|
|
5401
5620
|
};
|
|
5402
5621
|
// 累計配達回数を通算(絶対上限 RELAY_DELIVER_MAX の基準)。
|