@tyhld/conductor 0.12.84 → 0.12.85
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-flags.js +62 -0
- package/dist/cli.js +17 -1
- package/dist/headless.js +84 -1
- package/dist/relay.js +51 -0
- package/dist/selfcheck.js +743 -0
- package/package.json +2 -1
- package/scripts/dist-source.sh +60 -10
- package/scripts/hooks/skill_sync_notify.py +8 -1
- package/scripts/hooks/test_skill_sync_notify.py +8 -0
- package/scripts/tyhld-selfcheck.sh +58 -0
- package/scripts/update-guard.sh +2 -1
- package/systemd/tyhld-selfcheck.service +23 -0
- package/systemd/tyhld-selfcheck.timer +19 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自動更新の土台(便 b7bd5e43・設計 3ed9e2b2 の A2)— 「入れ替え待ちの札」と「Claude Code の指名」。
|
|
3
|
+
*
|
|
4
|
+
* ★どちらも【置くのは更新係(次の便 A3)か人】。中継と自己診断はここを読むだけ。
|
|
5
|
+
*
|
|
6
|
+
* 【置き場は ~/.tyhld/bin/autoupdate/(職人が書けない場所)】
|
|
7
|
+
* 設計の案は札を ~/.local/state/conductor/drain、指名を ~/.tyhld/claude-pin に置く形だった。
|
|
8
|
+
* 実機と番人の判定で確かめると(2026-10-02・このPC):
|
|
9
|
+
* ・~/.local/state/conductor/ … 職人の Bash から書けて消せる。番人も止めない。
|
|
10
|
+
* ・~/.tyhld/ の直下 … Write・Edit の道具は番人が止めるが、Bash の touch・rm・
|
|
11
|
+
* リダイレクト・python などは番人が通す(守る場所の一覧
|
|
12
|
+
* GUARD_PLACES に入っているのは ~/.tyhld/hooks と ~/.tyhld/bin だけ)。
|
|
13
|
+
* ・~/.tyhld/bin/ の下 … どの書き方でも番人が止める(下の階層も含む)。
|
|
14
|
+
* 職人が札を【置ける】と、自分で全現場の便を止められる。【外せる】と、入れ替えの途中に
|
|
15
|
+
* 便を入れられ、起こし直しで作業中の職人ごと止まる(試行回数を1つ使う)。
|
|
16
|
+
* ★番人の判定は変えない(この便の決め)ので、もう守られている ~/.tyhld/bin の下に置く。
|
|
17
|
+
* 更新係は systemd の中で動き、番人を通らないので書ける(置き場は更新係と同じ ~/.tyhld/bin)。
|
|
18
|
+
* ★囲い(sandbox)の効いている現場では、どちらの置き場もそもそも書けない(自己診断④で確かめる)。
|
|
19
|
+
*
|
|
20
|
+
* 【試験用の入口】★安全境界ではない(ほかの CONDUCTOR_* と同じ)。
|
|
21
|
+
* CONDUCTOR_DRAIN_FILE … 札の場所を差し替える
|
|
22
|
+
* CONDUCTOR_CLAUDE_PIN_FILE … 指名の場所を差し替える
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { autoupdateDir } from './relay.js';
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// 入れ替え待ちの札
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// ★札を読む本体は relay.ts に置く(relay.ts は試験が src から直接読み込むので、
|
|
31
|
+
// ほかの src を import できない=自分の中で完結させる)。ここは同じ物を見せるだけ。
|
|
32
|
+
export { autoupdateDir, drainFilePath, readDrain } from './relay.js';
|
|
33
|
+
/** 生存報告に載せる形(札が在るときだけ)。 */
|
|
34
|
+
export function drainReport(d, now = Date.now()) {
|
|
35
|
+
if (!d.active)
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
drain: true,
|
|
39
|
+
drainSince: d.sinceMs === null ? null : new Date(d.sinceMs).toISOString(),
|
|
40
|
+
drainSec: d.sinceMs === null ? null : Math.max(0, Math.round((now - d.sinceMs) / 1000)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Claude Code の指名
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
/** 指名の場所。中身は1行目=claude の実行ファイルの絶対パス(それ以外の行は読まない)。 */
|
|
47
|
+
export function claudePinPath() {
|
|
48
|
+
return process.env.CONDUCTOR_CLAUDE_PIN_FILE || path.join(autoupdateDir(), 'claude-pin');
|
|
49
|
+
}
|
|
50
|
+
/** 指名のファイルの1行目を読む。無ければ undefined、読めなければ null。 */
|
|
51
|
+
export function readClaudePinLine(file = claudePinPath()) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = readFileSync(file, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (e?.code === 'ENOENT')
|
|
58
|
+
return undefined;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return (raw.split('\n')[0] ?? '').trim();
|
|
62
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { RELAY_FETCH_TIMEOUT_MS, createRelayState, deliveryReport, parseAgentKind, reconnectDelayMs, relayTick, tmuxHasSession, pendingReportIds, sendPendingReportFiles, sweepProcessedReports, } from './relay.js';
|
|
18
|
-
import { createHeadlessState, headlessNextDelayMs, headlessTick, headlessTrackedCommandIds, parseRunnerMode, } from './headless.js';
|
|
18
|
+
import { createHeadlessState, headlessNextDelayMs, headlessTick, headlessTrackedCommandIds, evaluateClaudePin, lastClaudePinStatus, parseRunnerMode, } from './headless.js';
|
|
19
19
|
import { runViewCommand } from './view.js';
|
|
20
20
|
import { createRelayRunner } from './relay-runner.js';
|
|
21
21
|
import { installProcessGuards } from './process-guard.js';
|
|
@@ -24,6 +24,8 @@ import { startEar } from './ear.js';
|
|
|
24
24
|
import { loadEarConfig } from './ear-routing.js';
|
|
25
25
|
import { ensureEnvFilePermissions } from './env-file-perm.js';
|
|
26
26
|
import { createCentralPorts } from './build-central.js';
|
|
27
|
+
import { drainReport, readDrain } from './autoupdate-flags.js';
|
|
28
|
+
import { runSelfcheckCommand } from './selfcheck.js';
|
|
27
29
|
import { BUILD_NUDGE_NAME, REQUIRED_ENV, createBuildRunner, createMemoStore, loadBuildRunnerConfig, } from './build-runner.js';
|
|
28
30
|
/**
|
|
29
31
|
* relay の状態の参照(生存報告に「いま配れるか」を載せるため)。
|
|
@@ -199,6 +201,14 @@ async function sendReport(cfg) {
|
|
|
199
201
|
if (d.waitingSec !== null)
|
|
200
202
|
body.waitingSec = d.waitingSec;
|
|
201
203
|
}
|
|
204
|
+
// ★入れ替え待ちの札と Claude Code の指名(便 b7bd5e43)。どちらも【在るときだけ】載せる
|
|
205
|
+
// =札も指名も無い機の本文は今までと1バイトも変わらない。
|
|
206
|
+
const drain = drainReport(readDrain());
|
|
207
|
+
if (drain)
|
|
208
|
+
Object.assign(body, drain);
|
|
209
|
+
const pin = process.env.CONDUCTOR_CLAUDE_BIN ? lastClaudePinStatus() : evaluateClaudePin().status;
|
|
210
|
+
if (pin.state !== 'none')
|
|
211
|
+
body.claudePin = pin;
|
|
202
212
|
// ★未処理の報告(まだ管制へ渡せていない控え)の指示IDを載せる(便 57c04e6d)。
|
|
203
213
|
// 1件も無ければ載せない=今までと同じ本文。多すぎるときは上限までにして総数を添える。
|
|
204
214
|
const pending = pendingReportIds();
|
|
@@ -489,6 +499,8 @@ function printUsage() {
|
|
|
489
499
|
' conductor build-runner この機に1本の「司令塔」(試作の段取り役)',
|
|
490
500
|
' conductor setup <現場名> [フォルダ] [--profile <default|proto>] 采配くんをこのPCに設置する(初回の1回だけ)',
|
|
491
501
|
' conductor view <現場名> [--once] [--detail] その現場の今を見るだけの画面(kc の実体)',
|
|
502
|
+
' conductor selfcheck [--trigger <daily|after-conductor|after-claude|after-rollback|manual>] [--json] [--no-send]',
|
|
503
|
+
' この機の自己診断(副作用なし・結果は手元の控えと管制へ)',
|
|
492
504
|
'',
|
|
493
505
|
'見るだけの画面(conductor view = kc 現場名):',
|
|
494
506
|
' いま職人が何をしているかを、やさしい日本語で見るだけの画面です。',
|
|
@@ -574,6 +586,10 @@ async function main() {
|
|
|
574
586
|
// ★引数はそのまま setup.sh へ渡す(judgement は全部インストーラ側)。
|
|
575
587
|
runSetupCommand(process.argv.slice(3));
|
|
576
588
|
break;
|
|
589
|
+
case 'selfcheck':
|
|
590
|
+
// ★自己診断(便 b7bd5e43・A1)。副作用なし(commit・push・PR・合流・配備・公開をしない)。
|
|
591
|
+
process.exit(await runSelfcheckCommand(process.argv.slice(3)));
|
|
592
|
+
break;
|
|
577
593
|
case 'view':
|
|
578
594
|
// ★見るだけの画面。職人も tmux も relay も起動せず、入力も受け付けない。
|
|
579
595
|
// 既定は「開いたまま流れ続ける」ので、Ctrl+C まで戻らない(--once は1回で戻る)。
|
package/dist/headless.js
CHANGED
|
@@ -19,7 +19,8 @@ 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, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
22
|
+
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
23
|
+
import { claudePinPath, readClaudePinLine, } from './autoupdate-flags.js';
|
|
23
24
|
const log = (msg) => {
|
|
24
25
|
console.log(`[headless] ${msg}`);
|
|
25
26
|
};
|
|
@@ -352,6 +353,64 @@ export function claudeUpdateHint(bin) {
|
|
|
352
353
|
}
|
|
353
354
|
return `入れ方を見分けられませんでした。ネイティブ導入なら "${bin}" update / npm 導入なら npm install -g @anthropic-ai/claude-code@latest`;
|
|
354
355
|
}
|
|
356
|
+
// ── Claude Code の指名(~/.tyhld/bin/autoupdate/claude-pin・便 b7bd5e43) ─────
|
|
357
|
+
//
|
|
358
|
+
// 【なぜ要るか(設計 3ed9e2b2 の発見4)】中継はいちばん新しい claude を選ぶ。2つ入っている機では、
|
|
359
|
+
// 更新係が片方を元の版へ戻しても、もう片方の新しい版が選ばれて戻らない。更新係が「この実体を
|
|
360
|
+
// 使う」と指名できるようにする。★置くのは更新係(~/.tyhld/bin の下=番人がどの書き方でも止める=職人は書けない)。
|
|
361
|
+
// 【決め】指名が無い機は今までと1つも変わらない。CONDUCTOR_CLAUDE_BIN の指定は今までどおり最優先。
|
|
362
|
+
// 指名の実体が無い・読めない・最低版より古いときは、今までの選び方に戻り、その理由を管制へ届ける。
|
|
363
|
+
const CLAUDE_PIN_NONE = { state: 'none', pinned: null, reason: null, version: null };
|
|
364
|
+
/** 直近に resolveClaudeBin が決めた指名の扱い(生存報告と自己診断が読む)。 */
|
|
365
|
+
let lastClaudePin = CLAUDE_PIN_NONE;
|
|
366
|
+
export function lastClaudePinStatus() {
|
|
367
|
+
return lastClaudePin;
|
|
368
|
+
}
|
|
369
|
+
/** 指名を確かめた結果の控え(★同じ指名・同じ実体なら --version を打ち直さない)。 */
|
|
370
|
+
let pinCache = null;
|
|
371
|
+
/**
|
|
372
|
+
* 指名を確かめる。info が在れば指名どおりの claude を使ってよい。
|
|
373
|
+
* ★指名が無ければ { info: null, status: none }=呼び出し側は今までの選び方をそのまま使う。
|
|
374
|
+
*/
|
|
375
|
+
export function evaluateClaudePin(file = claudePinPath()) {
|
|
376
|
+
const line = readClaudePinLine(file);
|
|
377
|
+
if (line === undefined)
|
|
378
|
+
return { info: null, status: CLAUDE_PIN_NONE };
|
|
379
|
+
const fallback = (reason, pinned, version = null) => ({
|
|
380
|
+
info: null,
|
|
381
|
+
status: { state: 'fallback', pinned, reason: `${reason}。今までの選び方(いちばん新しい版)に戻りました`, version },
|
|
382
|
+
});
|
|
383
|
+
if (line === null)
|
|
384
|
+
return fallback('指名のファイルを読めません', null);
|
|
385
|
+
if (line === '')
|
|
386
|
+
return fallback('指名のファイルが空です', null);
|
|
387
|
+
if (!path.isAbsolute(line))
|
|
388
|
+
return fallback(`指名が絶対パスではありません(${line})`, line);
|
|
389
|
+
let key = '';
|
|
390
|
+
try {
|
|
391
|
+
const st = statSync(file);
|
|
392
|
+
const target = statSync(line);
|
|
393
|
+
key = `${file}:${st.mtimeMs}:${line}:${target.mtimeMs}:${target.size}`;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return fallback(`指名の claude がありません(${line})`, line);
|
|
397
|
+
}
|
|
398
|
+
if (pinCache && pinCache.key === key)
|
|
399
|
+
return pinCache.result;
|
|
400
|
+
const version = getClaudeVersion(line);
|
|
401
|
+
let result;
|
|
402
|
+
if (version === null) {
|
|
403
|
+
result = fallback(`指名の claude の版を読めません(${line})`, line);
|
|
404
|
+
}
|
|
405
|
+
else if (compareVersions(version, CLAUDE_MIN_VERSION) < 0) {
|
|
406
|
+
result = fallback(`指名の claude が最低版より古い(${line} = ${version}、必要な版 ${CLAUDE_MIN_VERSION} 以上)`, line, version);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
result = { info: { bin: line, version }, status: { state: 'used', pinned: line, reason: null, version } };
|
|
410
|
+
}
|
|
411
|
+
pinCache = { key, result };
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
355
414
|
/** ホームの外に在る、claude の既知の置き場(既定の候補)。 */
|
|
356
415
|
export const CLAUDE_SYSTEM_BIN_DIRS = ['/usr/local/bin', '/usr/bin'];
|
|
357
416
|
/** claude の実行ファイルの既知の候補を返す(新しい node の版から順)。 */
|
|
@@ -392,6 +451,8 @@ function claudeBinCandidates(systemDirs) {
|
|
|
392
451
|
*
|
|
393
452
|
* 探す順:
|
|
394
453
|
* 1. 環境変数 CONDUCTOR_CLAUDE_BIN(テスト用の差し替え口)→ 指定があればそれだけ
|
|
454
|
+
* 1b. 指名(~/.tyhld/bin/autoupdate/claude-pin)が在り、使える(在る・版が読める・最低版以上)ならそれ
|
|
455
|
+
* ★使えなければ黙って別の版を使わず、理由を lastClaudePinStatus() に残して 2 へ
|
|
395
456
|
* 2. 候補のうち実在する物の版を確かめ、いちばん新しい版を使う
|
|
396
457
|
* 3. 版を確かめられない物は後回し(版が分かる物が1つもないときだけ使う)
|
|
397
458
|
*
|
|
@@ -401,10 +462,21 @@ function claudeBinCandidates(systemDirs) {
|
|
|
401
462
|
export function resolveClaudeBin(systemDirs = CLAUDE_SYSTEM_BIN_DIRS) {
|
|
402
463
|
const override = process.env.CONDUCTOR_CLAUDE_BIN;
|
|
403
464
|
if (override) {
|
|
465
|
+
// ★今までどおり指定が最優先。指名が在っても見ない(在ることだけ管制へ届ける)。
|
|
466
|
+
lastClaudePin = readClaudePinLine() === undefined ? CLAUDE_PIN_NONE : {
|
|
467
|
+
state: 'override', pinned: null, version: null,
|
|
468
|
+
reason: 'CONDUCTOR_CLAUDE_BIN の指定があるので、指名は使っていません',
|
|
469
|
+
};
|
|
404
470
|
if (!existsSync(override))
|
|
405
471
|
return null;
|
|
406
472
|
return { bin: override, version: getClaudeVersion(override) };
|
|
407
473
|
}
|
|
474
|
+
// ★指名(~/.tyhld/bin/autoupdate/claude-pin)が在れば最初に選ぶ(便 b7bd5e43・A2)。
|
|
475
|
+
// 使えないときは黙って別の版を使わず、今までの選び方に戻ったことと理由を残す(管制へ届く)。
|
|
476
|
+
const pinned = evaluateClaudePin();
|
|
477
|
+
lastClaudePin = pinned.status;
|
|
478
|
+
if (pinned.info)
|
|
479
|
+
return pinned.info;
|
|
408
480
|
const existing = claudeBinCandidates(systemDirs).filter(c => existsSync(c));
|
|
409
481
|
if (existing.length === 0)
|
|
410
482
|
return null;
|
|
@@ -838,6 +910,11 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
838
910
|
return null;
|
|
839
911
|
}
|
|
840
912
|
const { bin: claudeBin, version: claudeVersion } = resolved;
|
|
913
|
+
const pin = lastClaudePinStatus();
|
|
914
|
+
if (pin.state === 'fallback')
|
|
915
|
+
log(`WARN Claude Code の指名を使えません: ${pin.reason}`);
|
|
916
|
+
else if (pin.state === 'used')
|
|
917
|
+
log(`Claude Code の指名どおりに使います: ${pin.pinned}`);
|
|
841
918
|
// ── 最低版チェック ──
|
|
842
919
|
if (claudeVersion && compareVersions(claudeVersion, CLAUDE_MIN_VERSION) < 0) {
|
|
843
920
|
// ★候補のうちいちばん新しい物でも最低版より古い=全部が古いときだけここに来る
|
|
@@ -1569,6 +1646,12 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
1569
1646
|
`自動再開しました (session=${sessionId}, attempt ${retry.attempt}/${HEADLESS_MAX_ATTEMPTS})`);
|
|
1570
1647
|
return;
|
|
1571
1648
|
}
|
|
1649
|
+
// ── 入れ替え待ちの札(便 b7bd5e43・A2)──
|
|
1650
|
+
// 札が在るあいだは新しい便を【取りに行かない】。管制に積まれた便はそのまま待つ=
|
|
1651
|
+
// 失われない・失敗にならない・試行回数も配達回数も使わない。札が外れたら次の周で今までどおり取る。
|
|
1652
|
+
// ★走っている便(state.job)と自動再開(pendingRetry)は上で済んでいる=止めない。
|
|
1653
|
+
if (!noteDrain(rs, cfg.site))
|
|
1654
|
+
return;
|
|
1572
1655
|
// ── 新規指示の取得 ──
|
|
1573
1656
|
const cmd = await fetchCommand(cfg);
|
|
1574
1657
|
if (!cmd) {
|
package/dist/relay.js
CHANGED
|
@@ -231,6 +231,7 @@ export function createRelayState() {
|
|
|
231
231
|
running: false,
|
|
232
232
|
restored: false,
|
|
233
233
|
lastHalted: null,
|
|
234
|
+
lastDrain: null,
|
|
234
235
|
lastSkippedId: null,
|
|
235
236
|
skipCount: 0,
|
|
236
237
|
skipWarned: false,
|
|
@@ -245,6 +246,53 @@ export function createRelayState() {
|
|
|
245
246
|
readiness: createReadiness(),
|
|
246
247
|
};
|
|
247
248
|
}
|
|
249
|
+
// ── 入れ替え待ちの札(便 b7bd5e43・設計 3ed9e2b2 の A2)─────────────────────
|
|
250
|
+
// 置き場の決め方と理由は src/autoupdate-flags.ts の冒頭(★~/.tyhld/bin の下=職人は書けない)。
|
|
251
|
+
// ★本体をここに置くのは、relay.ts を試験が src から直接読み込むため(ほかの src を import しない)。
|
|
252
|
+
/** 札と指名の置き場(★番人がどの書き方でも書き込みを止める ~/.tyhld/bin の下)。 */
|
|
253
|
+
export function autoupdateDir() {
|
|
254
|
+
return path.join(os.homedir(), '.tyhld', 'bin', 'autoupdate');
|
|
255
|
+
}
|
|
256
|
+
/** 札の場所(★置くのは更新係。中身は読まない=在るか無いかだけで決める)。 */
|
|
257
|
+
export function drainFilePath() {
|
|
258
|
+
return process.env.CONDUCTOR_DRAIN_FILE || path.join(autoupdateDir(), 'drain');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 札を読む。★読めない(権限など)ときも「在る」と扱う=迷ったら新しい便を取らない側。
|
|
262
|
+
* 便は管制で待つだけで、失われない・失敗にならない・試行回数も使わない。
|
|
263
|
+
*/
|
|
264
|
+
export function readDrain(file = drainFilePath()) {
|
|
265
|
+
try {
|
|
266
|
+
const st = statSync(file);
|
|
267
|
+
return { active: true, sinceMs: st.mtimeMs };
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
if (e?.code === 'ENOENT')
|
|
271
|
+
return { active: false, sinceMs: null };
|
|
272
|
+
return { active: true, sinceMs: null };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 入れ替え待ちの札を見る(便 b7bd5e43・A2)。返り true=新しい便を取りに行ってよい。
|
|
277
|
+
* ★画面あり・画面なしの両方式が、新しい便を取る直前にこれを呼ぶ(走っている便には効かない)。
|
|
278
|
+
* ★置かれた/外れたの変わり目だけ1行出す。
|
|
279
|
+
*/
|
|
280
|
+
export function noteDrain(state, site, now = Date.now()) {
|
|
281
|
+
const d = readDrain();
|
|
282
|
+
if (d.active) {
|
|
283
|
+
if (state.lastDrain !== true) {
|
|
284
|
+
log(`site=${site} 入れ替え待ちの札があるため、新しい便を取りません(走っている便は止めません・積まれた便は管制で待ちます)`);
|
|
285
|
+
}
|
|
286
|
+
state.lastDrain = true;
|
|
287
|
+
noteReadiness(state, 'draining', null, now);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (state.lastDrain === true) {
|
|
291
|
+
log(`site=${site} 入れ替え待ちの札が外れました。便の受け取りを再開します`);
|
|
292
|
+
}
|
|
293
|
+
state.lastDrain = false;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
248
296
|
/** 「指示は在るのに配れていない」状態(この間が未配達の時間)。 */
|
|
249
297
|
const BLOCKED_STATES = new Set([
|
|
250
298
|
'busy',
|
|
@@ -3512,6 +3560,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3512
3560
|
log('停止が解除されました。relayを再開します');
|
|
3513
3561
|
}
|
|
3514
3562
|
state.lastHalted = false;
|
|
3563
|
+
// ── 入れ替え待ちの札(便 b7bd5e43)──札が在るあいだは新しい便を取りに行かない。
|
|
3564
|
+
if (!noteDrain(state, cfg.site))
|
|
3565
|
+
return;
|
|
3515
3566
|
// ── 新規指示の取得 ──
|
|
3516
3567
|
const cmd = await fetchCommand(cfg);
|
|
3517
3568
|
if (!cmd) {
|