@tyhld/conductor 0.12.46 → 0.12.48

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, deliveryReport, parseAgentKind, reconnectDelayMs, relayTick, tmuxHasSession, pendingReportIds, sweepProcessedReports, } from './relay.js';
16
+ import { RELAY_FETCH_TIMEOUT_MS, createRelayState, deliveryReport, parseAgentKind, reconnectDelayMs, relayTick, tmuxHasSession, pendingReportIds, sendPendingReportFiles, sweepProcessedReports, } 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';
@@ -212,6 +212,12 @@ async function sendReport(cfg) {
212
212
  const msg = err instanceof Error ? err.message : String(err);
213
213
  console.log(`[conductor] ${time} site=${cfg.site} status=${cfg.status} -> 送信失敗: ${msg}`);
214
214
  }
215
+ // ★未処理の報告ファイルの本文を中央へ届ける(便 79840008)。受け取られた物だけ処理済みにする(消さない)。
216
+ // ★配達役がいま追っている便の報告は送らない(完了の道が完了と一緒に渡すため)。
217
+ if (pending.ids.length > 0) {
218
+ const tracking = relayStateForReport?.pending?.id;
219
+ await sendPendingReportFiles(cfg, { exclude: tracking ? [tracking] : [] });
220
+ }
215
221
  }
216
222
  /** `conductor start`: 起動直後に1回 + 以後 intervalSec ごとに報告し続ける。 */
217
223
  async function start(args) {
package/dist/relay.js CHANGED
@@ -204,6 +204,12 @@ export const BUSY_GLYPHS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '
204
204
  export function kindOf(cfg) {
205
205
  return cfg.agentKind ?? 'claude';
206
206
  }
207
+ /**
208
+ * 確定判断の件数と字数の上限(★中央 ai-kana の DISPATCH_CONTEXT_LIMITS と同じ値にすること)。
209
+ * 中央が守っているが、配り役でも同じ上限で守る(受け取った物が多すぎても本文を太らせない)。
210
+ */
211
+ export const DISPATCH_DECISIONS_MAX = 10;
212
+ export const DISPATCH_DECISION_CHARS = 300;
207
213
  /** 連続スキップ何回でアイドル誤判定の疑いをWARNするか(観測のみ・送信挙動は変えない)。 */
208
214
  export const RELAY_SKIP_WARN_THRESHOLD = 6;
209
215
  /** 初期状態を作る。 */
@@ -988,7 +994,7 @@ export function selectOrphanToRestore(orphans, currentPending) {
988
994
  * 画面では送信エコーの末尾に出る=完了検出(hasDoneBelowAnchor)の境界になる。
989
995
  * 末尾に置く理由: アンカーより上に来る指示文中の "DONE:{site}" を完了検出から確実に除外するため。
990
996
  */
991
- export function buildSendText(body, site, anchor, kind = 'claude') {
997
+ export function buildSendText(body, site, anchor, kind = 'claude', context) {
992
998
  // anchor から素の commandId を取り出す(conductor_complete に渡す引数。接頭辞無しなので新たな
993
999
  // アンカー一致を作らず、sliceBelowAnchor の境界判定に干渉しない)。
994
1000
  const commandId = anchor.startsWith(`${SEND_ANCHOR_PREFIX}:`)
@@ -997,12 +1003,31 @@ export function buildSendText(body, site, anchor, kind = 'claude') {
997
1003
  // ★Claude の職人には「便の終え方」の1行だけを付ける(書き方の正本は鉄則の「便の終え方」の節)。
998
1004
  // ★Codex の職人には今までどおり全文を付ける。鉄則(~/.claude/skills)は Codex には届かないため。
999
1005
  const guide = kind === 'codex' ? fullCompletionGuide(site, commandId) : shortCompletionLine(site, commandId);
1000
- const full = `${body}\n\n${guide}` +
1006
+ const full = `${body}\n\n${dispatchContextBlock(context)}${guide}` +
1001
1007
  // アンカー(管理用ジョブ印)は必ず本文の最後に置く。完了検出の境界として使う。
1002
1008
  `(管理用ジョブ印・変更不要: ${anchor})`;
1003
1009
  // 改行(と連続改行)を空白1個に潰し、前後を整える。
1004
1010
  return full.replace(/\r?\n+/g, ' ').replace(/\s+/g, ' ').trim();
1005
1011
  }
1012
+ /**
1013
+ * 本文に添える「親の便」と「この便の確定判断」の段(便 4147c519)。
1014
+ * ★中央の口から受け取った物だけ。どちらも無ければ空文字(本文は1文字も変わらない)。
1015
+ * ★ほかの便の報告全文・再開パック・過去会話は入れない(えふさん確定の「必要最小限」)。
1016
+ */
1017
+ export function dispatchContextBlock(context) {
1018
+ if (!context)
1019
+ return '';
1020
+ const lines = [];
1021
+ if (context.parentCommandId) {
1022
+ lines.push(`【親の便】${context.parentCommandId}(この便は親の便の続きです)`);
1023
+ }
1024
+ const decisions = (context.decisions ?? []).filter((d) => d.trim().length > 0);
1025
+ if (decisions.length > 0) {
1026
+ lines.push('【この便の確定判断(管制から)】');
1027
+ decisions.forEach((d, i) => lines.push(`${i + 1}. ${d.trim()}`));
1028
+ }
1029
+ return lines.length > 0 ? `${lines.join('\n')}\n\n` : '';
1030
+ }
1006
1031
  /** 鉄則(conductor-craftsman の SKILL.md)の中の、便の終え方の見出し。1行の案内はここを指す。 */
1007
1032
  export const CRAFTSMAN_FINISH_HEADING = '## 便の終え方';
1008
1033
  /**
@@ -1357,6 +1382,95 @@ export function pendingReportIds(env, limit = PENDING_REPORTS_MAX) {
1357
1382
  const max = Math.max(0, Math.floor(limit));
1358
1383
  return { ids: sorted.slice(0, max), total: sorted.length };
1359
1384
  }
1385
+ // ───────────────────────────────────────────────────────────────────────────
1386
+ // 未処理の報告ファイルの本文を中央へ届ける(便 79840008・回収の残り②・中央は ai-kana PR #351)
1387
+ //
1388
+ // 【なぜ要るか】追跡が切れたあと(配達役の再起動・時間切れ・職人が自分で終えた等)に書かれた報告は、
1389
+ // 手元に残るだけで本文が中央へ届かなかった(生存報告には指示IDだけを載せていた=pendingReports)。
1390
+ // 中央に受け口 POST /api/conductor/report/files ができたので、ファイルの全文をそのまま送る。
1391
+ // 【決まり】
1392
+ // ・1回 REPORT_FILES_BATCH(20)件まで。多ければ分けて送る。
1393
+ // ・返りの results[i].ok === true の物だけ processed/ へ移す(★消さない)。ok:false と、送れなかった物は
1394
+ // 未処理のまま残す(次の周でやり直す)。理由はログに出す。
1395
+ // ・★配達役は完了にしない・便の状態の値を書かない(中央も完了にしない=止まった便を「回収必要」まで進めるだけ)。
1396
+ // ・★配達役がいま追っている便の報告は送らない。それは完了の道(completeWithReport)が読んで完了と一緒に
1397
+ // 渡す。先に processed/ へ移すと、completeWithReport が「渡し済み」とみなして完了を書かなくなる。
1398
+ // ───────────────────────────────────────────────────────────────────────────
1399
+ /** 1回に送る報告ファイルの件数の上限(★中央の FILE_INTAKE_LIMIT と同じ値にすること)。 */
1400
+ export const REPORT_FILES_BATCH = 20;
1401
+ /** 1周で送る件数の上限(多すぎる分は次の周に回す=落とさない)。 */
1402
+ export const REPORT_FILES_MAX_PER_ROUND = 100;
1403
+ /**
1404
+ * 未処理の報告ファイルの全文を中央へ送り、受け取られた物だけ処理済みにする。
1405
+ * 返り: 送った件数・処理済みにした件数・未処理のまま残した件数。
1406
+ */
1407
+ export async function sendPendingReportFiles(cfg, opts = {}) {
1408
+ const env = opts.env;
1409
+ const doFetch = opts.fetchImpl ?? fetch;
1410
+ const logLine = opts.logLine ?? log;
1411
+ const exclude = new Set(opts.exclude ?? []);
1412
+ const items = [];
1413
+ for (const id of pendingReportIds(env, REPORT_FILES_MAX_PER_ROUND).ids) {
1414
+ if (exclude.has(id))
1415
+ continue;
1416
+ for (const file of pendingCandidates(id, env)) {
1417
+ const raw = readIfFilled(file);
1418
+ if (raw !== null) {
1419
+ items.push({ id, raw });
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1424
+ let sent = 0;
1425
+ let processed = 0;
1426
+ let kept = 0;
1427
+ for (let i = 0; i < items.length; i += REPORT_FILES_BATCH) {
1428
+ const chunk = items.slice(i, i + REPORT_FILES_BATCH);
1429
+ let results = null;
1430
+ try {
1431
+ const res = await doFetch(`${cfg.url}/api/conductor/report/files`, {
1432
+ method: 'POST',
1433
+ headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
1434
+ body: JSON.stringify({ files: chunk.map((c) => c.raw) }),
1435
+ signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
1436
+ });
1437
+ sent += chunk.length;
1438
+ if (!res.ok) {
1439
+ logLine(`site=${cfg.site} 報告ファイルの送信 -> HTTP ${res.status}(${chunk.length}件は未処理のまま残す)`);
1440
+ }
1441
+ else {
1442
+ const data = (await res.json().catch(() => null));
1443
+ if (Array.isArray(data?.results) && data.results.length === chunk.length) {
1444
+ results = data.results;
1445
+ }
1446
+ else {
1447
+ logLine(`site=${cfg.site} 報告ファイルの送信 -> 返りの形が違う(${chunk.length}件は未処理のまま残す)`);
1448
+ }
1449
+ }
1450
+ }
1451
+ catch (err) {
1452
+ logLine(`site=${cfg.site} 報告ファイルの送信 -> 失敗: ${err instanceof Error ? err.message : String(err)}` +
1453
+ `(${chunk.length}件は未処理のまま残す)`);
1454
+ }
1455
+ for (let k = 0; k < chunk.length; k++) {
1456
+ const r = results?.[k];
1457
+ if (r && r.ok === true && markReportProcessed(chunk[k].id, env)) {
1458
+ processed++;
1459
+ }
1460
+ else {
1461
+ kept++;
1462
+ if (r) {
1463
+ const why = String(r.reason ?? r.error ?? '理由なし').slice(0, 200);
1464
+ logLine(`site=${cfg.site} id=${chunk[k].id} の報告ファイルは受け取られませんでした(未処理のまま残す): ${why}`);
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ if (items.length > 0) {
1470
+ logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件`);
1471
+ }
1472
+ return { sent, processed, kept };
1473
+ }
1360
1474
  /**
1361
1475
  * 古い控えを片付ける。★消すのは `processed/` の中だけ(未処理は何日たっても消さない)。
1362
1476
  * 返り: 消した件数。
@@ -1888,7 +2002,26 @@ export async function fetchCommand(cfg) {
1888
2002
  log(`site=${cfg.site} 指示の形式が不正のためスキップ`);
1889
2003
  return null;
1890
2004
  }
1891
- return { id: String(first.id), body: first.body };
2005
+ const cmd = { id: String(first.id), body: first.body };
2006
+ // ★親の便・確定判断は、中央が返した物だけを形を確かめて受け取る(違う形は捨てる)。
2007
+ if (typeof first.parentCommandId === 'string' && first.parentCommandId.trim()) {
2008
+ cmd.parentCommandId = first.parentCommandId.trim();
2009
+ }
2010
+ else if (first.parentCommandId === null) {
2011
+ cmd.parentCommandId = null;
2012
+ }
2013
+ if (Array.isArray(first.decisions)) {
2014
+ cmd.decisions = first.decisions
2015
+ .filter((d) => typeof d === 'string' && d.trim().length > 0)
2016
+ .slice(0, DISPATCH_DECISIONS_MAX)
2017
+ .map((d) => {
2018
+ const t = d.trim();
2019
+ return [...t].length > DISPATCH_DECISION_CHARS
2020
+ ? `${[...t].slice(0, DISPATCH_DECISION_CHARS - 1).join('')}…`
2021
+ : t;
2022
+ });
2023
+ }
2024
+ return cmd;
1892
2025
  }
1893
2026
  catch (err) {
1894
2027
  log(`site=${cfg.site} commands取得 -> 失敗: ${err instanceof Error ? err.message : String(err)}`);
@@ -2719,7 +2852,11 @@ export async function relayTick(cfg, state, hooks) {
2719
2852
  // 画面にエコーされたアンカー行を「今回ジョブの境界」として記録し、完了検出は
2720
2853
  // アンカーより下=今回ジョブの出力だけを見る(churn・過去ジョブ DONE に強い)。
2721
2854
  const anchor = buildAnchor(cmd.id);
2722
- const text = buildSendText(cmd.body, cfg.site, anchor, kindOf(cfg));
2855
+ // ★親の便と確定判断を添えるのは 1便1context のときだけ(便 4147c519)。既定オフの本文は1文字も変えない。
2856
+ const context = cfg.freshContext && kindOf(cfg) === 'claude'
2857
+ ? { parentCommandId: cmd.parentCommandId ?? null, decisions: cmd.decisions ?? [] }
2858
+ : undefined;
2859
+ const text = buildSendText(cmd.body, cfg.site, anchor, kindOf(cfg), context);
2723
2860
  const sendStartedAt = Date.now();
2724
2861
  if (!sendToTmux(cfg.site, text)) {
2725
2862
  // 送信失敗(sendToTmux は内部で例外も握って false を返す)。status は queued のまま据え置く。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.46",
3
+ "version": "0.12.48",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {