@tyhld/conductor 0.12.33 → 0.12.34
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 +18 -1
- package/dist/relay.js +51 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import os from 'node:os';
|
|
16
|
-
import { RELAY_FETCH_TIMEOUT_MS, createRelayState, parseAgentKind, reconnectDelayMs, relayTick, tmuxHasSession, } from './relay.js';
|
|
16
|
+
import { RELAY_FETCH_TIMEOUT_MS, createRelayState, deliveryReport, parseAgentKind, reconnectDelayMs, relayTick, tmuxHasSession, } from './relay.js';
|
|
17
17
|
import { createRelayRunner } from './relay-runner.js';
|
|
18
18
|
import { installProcessGuards } from './process-guard.js';
|
|
19
19
|
import { registerNudgeTarget, unregisterNudgeTarget, watchNudges, } from './nudge.js';
|
|
@@ -22,6 +22,11 @@ import { loadEarConfig } from './ear-routing.js';
|
|
|
22
22
|
import { ensureEnvFilePermissions } from './env-file-perm.js';
|
|
23
23
|
import { createCentralPorts } from './build-central.js';
|
|
24
24
|
import { BUILD_NUDGE_NAME, REQUIRED_ENV, createBuildRunner, createMemoStore, loadBuildRunnerConfig, } from './build-runner.js';
|
|
25
|
+
/**
|
|
26
|
+
* relay の状態の参照(生存報告に「いま配れるか」を載せるため)。
|
|
27
|
+
* ★relay が無効な機では null のまま=報告の中身は今までと1バイトも変わらない。
|
|
28
|
+
*/
|
|
29
|
+
let relayStateForReport = null;
|
|
25
30
|
/** git コマンドを安全に実行し、失敗したら null を返す(git でない/未インストール等)。 */
|
|
26
31
|
function git(args) {
|
|
27
32
|
try {
|
|
@@ -161,6 +166,16 @@ async function sendReport(cfg) {
|
|
|
161
166
|
lastAction: cfg.lastAction,
|
|
162
167
|
artisanReady: tmuxHasSession(cfg.site),
|
|
163
168
|
};
|
|
169
|
+
// ★relay が動いている機だけ、2段目(いま配れるか)を足す。
|
|
170
|
+
// 未配達でないときは秒数と指示idを載せない=管制で「遅れ」を数えない。
|
|
171
|
+
if (relayStateForReport) {
|
|
172
|
+
const d = deliveryReport(relayStateForReport);
|
|
173
|
+
body.deliveryState = d.deliveryState;
|
|
174
|
+
if (d.undeliveredSec !== null)
|
|
175
|
+
body.undeliveredSec = d.undeliveredSec;
|
|
176
|
+
if (d.queuedCommandId !== null)
|
|
177
|
+
body.queuedCommandId = d.queuedCommandId;
|
|
178
|
+
}
|
|
164
179
|
const time = new Date().toLocaleTimeString();
|
|
165
180
|
try {
|
|
166
181
|
const res = await fetch(`${cfg.url}/api/conductor/report`, {
|
|
@@ -222,6 +237,8 @@ async function start(args) {
|
|
|
222
237
|
`(承認待ちの間だけ ${cfg.promptIntervalSec}s に短縮)` +
|
|
223
238
|
`(tmuxセッション "${cfg.site}" がアイドルのときだけ送信します)`);
|
|
224
239
|
const relayState = createRelayState();
|
|
240
|
+
// 生存報告に「いま配れるか」を載せるための参照(便 600d2a6a)。
|
|
241
|
+
relayStateForReport = relayState;
|
|
225
242
|
relayRunner = createRelayRunner({
|
|
226
243
|
tick: () => relayTick(cfg, relayState, {
|
|
227
244
|
// ★管制へつながらない状態から戻った瞬間に1回だけ呼ばれる。
|
package/dist/relay.js
CHANGED
|
@@ -223,6 +223,47 @@ export function createRelayState() {
|
|
|
223
223
|
promptPending: false,
|
|
224
224
|
busyRaisedId: null,
|
|
225
225
|
central: { failures: 0, detail: '', downSince: null },
|
|
226
|
+
readiness: createReadiness(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** 「指示は在るのに配れていない」状態(この間が未配達の時間)。 */
|
|
230
|
+
const BLOCKED_STATES = new Set([
|
|
231
|
+
'busy',
|
|
232
|
+
'no-session',
|
|
233
|
+
'not-agent',
|
|
234
|
+
]);
|
|
235
|
+
export function createReadiness(now = Date.now()) {
|
|
236
|
+
return { state: 'unknown', commandId: null, since: now, skipCount: 0 };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 1周の結末を記録する。同じ結末が続く間は since を動かさない(=そこから何秒経ったかが分かる)。
|
|
240
|
+
* ★記録するだけ。配達の判断は1バイトも変えない。
|
|
241
|
+
*/
|
|
242
|
+
export function noteReadiness(state, next, commandId, now = Date.now()) {
|
|
243
|
+
const cur = state.readiness;
|
|
244
|
+
if (cur.state === next && cur.commandId === commandId) {
|
|
245
|
+
if (BLOCKED_STATES.has(next))
|
|
246
|
+
cur.skipCount += 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
state.readiness = {
|
|
250
|
+
state: next,
|
|
251
|
+
commandId,
|
|
252
|
+
since: now,
|
|
253
|
+
skipCount: BLOCKED_STATES.has(next) ? 1 : 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* 生存報告に載せる形。★「指示が在るのに配れていない」ときだけ、その長さと指示idを出す。
|
|
258
|
+
* 配れている・指示が無いときは undeliveredSec を null にする(管制で「遅れ」を数えない)。
|
|
259
|
+
*/
|
|
260
|
+
export function deliveryReport(state, now = Date.now()) {
|
|
261
|
+
const r = state.readiness;
|
|
262
|
+
const blocked = BLOCKED_STATES.has(r.state) && r.commandId !== null;
|
|
263
|
+
return {
|
|
264
|
+
deliveryState: r.state,
|
|
265
|
+
queuedCommandId: blocked ? r.commandId : null,
|
|
266
|
+
undeliveredSec: blocked ? Math.max(0, Math.round((now - r.since) / 1000)) : null,
|
|
226
267
|
};
|
|
227
268
|
}
|
|
228
269
|
/**
|
|
@@ -2045,6 +2086,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2045
2086
|
: 'relay停止中(緊急停止スイッチON)のため送信しません');
|
|
2046
2087
|
}
|
|
2047
2088
|
state.lastHalted = true;
|
|
2089
|
+
noteReadiness(state, 'halted', null);
|
|
2048
2090
|
return;
|
|
2049
2091
|
}
|
|
2050
2092
|
if (state.lastHalted === true) {
|
|
@@ -2053,8 +2095,10 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2053
2095
|
state.lastHalted = false;
|
|
2054
2096
|
// ── 新規指示の取得 ──
|
|
2055
2097
|
const cmd = await fetchCommand(cfg);
|
|
2056
|
-
if (!cmd)
|
|
2098
|
+
if (!cmd) {
|
|
2099
|
+
noteReadiness(state, 'no-command', null);
|
|
2057
2100
|
return; // 指示なし
|
|
2101
|
+
}
|
|
2058
2102
|
// ── 配達放棄ガード(無限ループの構造的封じ込め) ──
|
|
2059
2103
|
// 配達回数上限を超えて放棄した command は、サーバがまだ queued を返しても(=完了通知が
|
|
2060
2104
|
// 単線で切れて詰まった状態でも)二度と配達しない。放棄時に既にWARNを出しているので静かにスキップ。
|
|
@@ -2073,6 +2117,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2073
2117
|
// ── アイドル判定(保守的・二重チェック) ──
|
|
2074
2118
|
if (!tmuxHasSession(cfg.site)) {
|
|
2075
2119
|
log(`site=${cfg.site} id=${cmd.id} tmux未起動のため送らずスキップ`);
|
|
2120
|
+
noteReadiness(state, 'no-session', cmd.id);
|
|
2076
2121
|
return;
|
|
2077
2122
|
}
|
|
2078
2123
|
if (!(await isIdleStable(cfg.site, kindOf(cfg)))) {
|
|
@@ -2084,6 +2129,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2084
2129
|
state.skipWarned = false;
|
|
2085
2130
|
}
|
|
2086
2131
|
state.skipCount += 1;
|
|
2132
|
+
// ★「指示は在るのに配れていない」ことを、この周の結末として記録する(便 600d2a6a)。
|
|
2133
|
+
// 生存報告に載せて、管制が「職人が作業中だから待っている」と言えるようにする。
|
|
2134
|
+
noteReadiness(state, 'busy', cmd.id);
|
|
2087
2135
|
log(`site=${cfg.site} id=${cmd.id} 作業中の可能性ありスキップ(次回再判定)`);
|
|
2088
2136
|
if (state.skipCount > RELAY_SKIP_WARN_THRESHOLD && !state.skipWarned) {
|
|
2089
2137
|
state.skipWarned = true;
|
|
@@ -2104,6 +2152,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2104
2152
|
if (guardCap === null || !looksLikeAgent(guardCap, kindOf(cfg))) {
|
|
2105
2153
|
log(`WARN site=${cfg.site} id=${cmd.id} Claude Codeの箱と確認できないため送信せずスキップ` +
|
|
2106
2154
|
'(名前一致でも非claude窓への誤爆を防止)');
|
|
2155
|
+
noteReadiness(state, 'not-agent', cmd.id);
|
|
2107
2156
|
return;
|
|
2108
2157
|
}
|
|
2109
2158
|
// ── tmux 送信 ──
|
|
@@ -2140,6 +2189,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2140
2189
|
markDelivered(state, cmd.id);
|
|
2141
2190
|
// 新しい指示の生存ハートビートは先頭から数え直す(前指示のスロットルを引き継がない)。
|
|
2142
2191
|
state.lastHeartbeatAt = null;
|
|
2192
|
+
noteReadiness(state, 'delivered', cmd.id);
|
|
2143
2193
|
log(`site=${cfg.site} id=${cmd.id} 配達しました`);
|
|
2144
2194
|
// ★配達の直後に「配達済み(sent)」を管制へ書く。書けなければ次tick以降で書けるまでやり直す
|
|
2145
2195
|
// (markSentAtCentral)。★書けなくても再配達はしない。
|