@tyhld/conductor 0.12.33 → 0.12.37

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 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)。★書けなくても再配達はしない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.33",
3
+ "version": "0.12.37",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {