@tyhld/conductor 0.12.45 → 0.12.47
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 +10 -1
- package/dist/relay.js +162 -0
- 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, 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';
|
|
@@ -121,6 +121,8 @@ function loadConfig(args) {
|
|
|
121
121
|
}
|
|
122
122
|
// ★職人の種類(ADR-021)。未設定・知らない値は 'claude'(後退させない)。
|
|
123
123
|
const agentKind = parseAgentKind(process.env.CONDUCTOR_AGENT_KIND);
|
|
124
|
+
// ★1便1context(便 87a1a28a)。既定オフ。1/true/yes/on のときだけ入る。
|
|
125
|
+
const freshContext = /^(1|true|yes|on)$/i.test((process.env.CONDUCTOR_FRESH_CONTEXT ?? '').trim());
|
|
124
126
|
// relay 設定(既定 off。off の間は既存の heartbeat 挙動を一切変えない)。
|
|
125
127
|
const relayEnabled = /^(1|true|yes|on)$/i.test((process.env.CONDUCTOR_RELAY_ENABLED ?? '').trim());
|
|
126
128
|
let relayIntervalSec = 15;
|
|
@@ -154,6 +156,7 @@ function loadConfig(args) {
|
|
|
154
156
|
relayIntervalSec,
|
|
155
157
|
promptIntervalSec,
|
|
156
158
|
agentKind,
|
|
159
|
+
freshContext,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
162
|
/** 1回分の報告を送る。送信結果(HTTPステータス)をログ表示する。 */
|
|
@@ -209,6 +212,12 @@ async function sendReport(cfg) {
|
|
|
209
212
|
const msg = err instanceof Error ? err.message : String(err);
|
|
210
213
|
console.log(`[conductor] ${time} site=${cfg.site} status=${cfg.status} -> 送信失敗: ${msg}`);
|
|
211
214
|
}
|
|
215
|
+
// ★未処理の報告ファイルの本文を中央へ届ける(便 79840008)。受け取られた物だけ処理済みにする(消さない)。
|
|
216
|
+
// ★配達役がいま追っている便の報告は送らない(完了の道が完了と一緒に渡すため)。
|
|
217
|
+
if (pending.ids.length > 0) {
|
|
218
|
+
const tracking = relayStateForReport?.pending?.id;
|
|
219
|
+
await sendPendingReportFiles(cfg, { exclude: tracking ? [tracking] : [] });
|
|
220
|
+
}
|
|
212
221
|
}
|
|
213
222
|
/** `conductor start`: 起動直後に1回 + 以後 intervalSec ごとに報告し続ける。 */
|
|
214
223
|
async function start(args) {
|
package/dist/relay.js
CHANGED
|
@@ -1357,6 +1357,95 @@ export function pendingReportIds(env, limit = PENDING_REPORTS_MAX) {
|
|
|
1357
1357
|
const max = Math.max(0, Math.floor(limit));
|
|
1358
1358
|
return { ids: sorted.slice(0, max), total: sorted.length };
|
|
1359
1359
|
}
|
|
1360
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1361
|
+
// 未処理の報告ファイルの本文を中央へ届ける(便 79840008・回収の残り②・中央は ai-kana PR #351)
|
|
1362
|
+
//
|
|
1363
|
+
// 【なぜ要るか】追跡が切れたあと(配達役の再起動・時間切れ・職人が自分で終えた等)に書かれた報告は、
|
|
1364
|
+
// 手元に残るだけで本文が中央へ届かなかった(生存報告には指示IDだけを載せていた=pendingReports)。
|
|
1365
|
+
// 中央に受け口 POST /api/conductor/report/files ができたので、ファイルの全文をそのまま送る。
|
|
1366
|
+
// 【決まり】
|
|
1367
|
+
// ・1回 REPORT_FILES_BATCH(20)件まで。多ければ分けて送る。
|
|
1368
|
+
// ・返りの results[i].ok === true の物だけ processed/ へ移す(★消さない)。ok:false と、送れなかった物は
|
|
1369
|
+
// 未処理のまま残す(次の周でやり直す)。理由はログに出す。
|
|
1370
|
+
// ・★配達役は完了にしない・便の状態の値を書かない(中央も完了にしない=止まった便を「回収必要」まで進めるだけ)。
|
|
1371
|
+
// ・★配達役がいま追っている便の報告は送らない。それは完了の道(completeWithReport)が読んで完了と一緒に
|
|
1372
|
+
// 渡す。先に processed/ へ移すと、completeWithReport が「渡し済み」とみなして完了を書かなくなる。
|
|
1373
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1374
|
+
/** 1回に送る報告ファイルの件数の上限(★中央の FILE_INTAKE_LIMIT と同じ値にすること)。 */
|
|
1375
|
+
export const REPORT_FILES_BATCH = 20;
|
|
1376
|
+
/** 1周で送る件数の上限(多すぎる分は次の周に回す=落とさない)。 */
|
|
1377
|
+
export const REPORT_FILES_MAX_PER_ROUND = 100;
|
|
1378
|
+
/**
|
|
1379
|
+
* 未処理の報告ファイルの全文を中央へ送り、受け取られた物だけ処理済みにする。
|
|
1380
|
+
* 返り: 送った件数・処理済みにした件数・未処理のまま残した件数。
|
|
1381
|
+
*/
|
|
1382
|
+
export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
1383
|
+
const env = opts.env;
|
|
1384
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
1385
|
+
const logLine = opts.logLine ?? log;
|
|
1386
|
+
const exclude = new Set(opts.exclude ?? []);
|
|
1387
|
+
const items = [];
|
|
1388
|
+
for (const id of pendingReportIds(env, REPORT_FILES_MAX_PER_ROUND).ids) {
|
|
1389
|
+
if (exclude.has(id))
|
|
1390
|
+
continue;
|
|
1391
|
+
for (const file of pendingCandidates(id, env)) {
|
|
1392
|
+
const raw = readIfFilled(file);
|
|
1393
|
+
if (raw !== null) {
|
|
1394
|
+
items.push({ id, raw });
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
let sent = 0;
|
|
1400
|
+
let processed = 0;
|
|
1401
|
+
let kept = 0;
|
|
1402
|
+
for (let i = 0; i < items.length; i += REPORT_FILES_BATCH) {
|
|
1403
|
+
const chunk = items.slice(i, i + REPORT_FILES_BATCH);
|
|
1404
|
+
let results = null;
|
|
1405
|
+
try {
|
|
1406
|
+
const res = await doFetch(`${cfg.url}/api/conductor/report/files`, {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
1409
|
+
body: JSON.stringify({ files: chunk.map((c) => c.raw) }),
|
|
1410
|
+
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
|
1411
|
+
});
|
|
1412
|
+
sent += chunk.length;
|
|
1413
|
+
if (!res.ok) {
|
|
1414
|
+
logLine(`site=${cfg.site} 報告ファイルの送信 -> HTTP ${res.status}(${chunk.length}件は未処理のまま残す)`);
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
const data = (await res.json().catch(() => null));
|
|
1418
|
+
if (Array.isArray(data?.results) && data.results.length === chunk.length) {
|
|
1419
|
+
results = data.results;
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
logLine(`site=${cfg.site} 報告ファイルの送信 -> 返りの形が違う(${chunk.length}件は未処理のまま残す)`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
catch (err) {
|
|
1427
|
+
logLine(`site=${cfg.site} 報告ファイルの送信 -> 失敗: ${err instanceof Error ? err.message : String(err)}` +
|
|
1428
|
+
`(${chunk.length}件は未処理のまま残す)`);
|
|
1429
|
+
}
|
|
1430
|
+
for (let k = 0; k < chunk.length; k++) {
|
|
1431
|
+
const r = results?.[k];
|
|
1432
|
+
if (r && r.ok === true && markReportProcessed(chunk[k].id, env)) {
|
|
1433
|
+
processed++;
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
kept++;
|
|
1437
|
+
if (r) {
|
|
1438
|
+
const why = String(r.reason ?? r.error ?? '理由なし').slice(0, 200);
|
|
1439
|
+
logLine(`site=${cfg.site} id=${chunk[k].id} の報告ファイルは受け取られませんでした(未処理のまま残す): ${why}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (items.length > 0) {
|
|
1445
|
+
logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件`);
|
|
1446
|
+
}
|
|
1447
|
+
return { sent, processed, kept };
|
|
1448
|
+
}
|
|
1360
1449
|
/**
|
|
1361
1450
|
* 古い控えを片付ける。★消すのは `processed/` の中だけ(未処理は何日たっても消さない)。
|
|
1362
1451
|
* 返り: 消した件数。
|
|
@@ -1694,6 +1783,66 @@ export function hasPendingDeliveryInInput(cap) {
|
|
|
1694
1783
|
}
|
|
1695
1784
|
return block.join('\n').includes('(配達役より)');
|
|
1696
1785
|
}
|
|
1786
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1787
|
+
// 1便1context:配る直前に職人の会話を新しくする(便 87a1a28a・★既定オフ)
|
|
1788
|
+
//
|
|
1789
|
+
// 【なぜ /clear か(試験用の tmux で本物の Claude Code に当てて実測 2026-09-21)】
|
|
1790
|
+
// ・/clear で会話の記録のファイルが新しくなる(新しい会話の番号・前のファイルは閉じる)。
|
|
1791
|
+
// 終わりと始まりのフック(SessionEnd:clear・SessionStart:clear)も鳴る=旧context の終了。
|
|
1792
|
+
// ・/clear のあとの開始時の背負いは 59,849 トークン=起動した直後(59,713)とほぼ同じ。前の会話は持ち越さない。
|
|
1793
|
+
// ・職人を止めないので、囲いのふた(6bef35c8)も起動待ちも起きない。
|
|
1794
|
+
// ★道具のつなぎ直しはしない(起動時の「つなぎ始め」の記録が /clear のあとには無い)=あとから足した
|
|
1795
|
+
// 口は /clear では使えるようにならない。それが要る日は起こし直し(restart-cc2.sh)を使う。
|
|
1796
|
+
// 【新しくできたかの確かめ】受付側の状態で見る:その作業場所の会話の記録に、打つ前には無かった
|
|
1797
|
+
// ファイルができたか。確かめられなければ false(呼び出し側は配らない)。
|
|
1798
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1799
|
+
/** 会話を新しくできたかを待つ長さ(ミリ秒)。 */
|
|
1800
|
+
export const RELAY_FRESH_CONFIRM_MS = 10_000;
|
|
1801
|
+
/**
|
|
1802
|
+
* 会話が新しくなってから配るまでに置く間(ミリ秒)。
|
|
1803
|
+
* ★実測(試験用の tmux・本物の Claude Code):新しい会話のファイルができた1秒後に貼り付けると、
|
|
1804
|
+
* 入力欄がまだ整っておらず【貼り付けた本文が落ち、添え書きだけが送信された】(職人は「貼り付けが
|
|
1805
|
+
* 届いていない」と答えて止まった)。間を置いたうえで、画面が入力待ちで落ち着いたことも確かめる。
|
|
1806
|
+
*/
|
|
1807
|
+
export const RELAY_FRESH_SETTLE_MS = 3_000;
|
|
1808
|
+
/** その作業場所の会話の記録のファイル名の一覧。読めなければ空。 */
|
|
1809
|
+
export function listTranscripts(cwd, env = process.env) {
|
|
1810
|
+
try {
|
|
1811
|
+
return readdirSync(claudeTranscriptDir(cwd, env)).filter((n) => n.endsWith('.jsonl'));
|
|
1812
|
+
}
|
|
1813
|
+
catch {
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
export async function freshenContext(site, cwd, d = {
|
|
1818
|
+
listSessions: () => listTranscripts(cwd),
|
|
1819
|
+
type: (text) => runTmux(['send-keys', '-t', site, '-l', text]),
|
|
1820
|
+
enter: () => runTmux(['send-keys', '-t', site, 'Enter']),
|
|
1821
|
+
pause: pauseSync,
|
|
1822
|
+
sleep,
|
|
1823
|
+
ready: () => isIdleStable(site, 'claude'),
|
|
1824
|
+
}, o = { waitMs: RELAY_FRESH_CONFIRM_MS, pollMs: 500 }) {
|
|
1825
|
+
const before = new Set(d.listSessions());
|
|
1826
|
+
try {
|
|
1827
|
+
d.type('/clear');
|
|
1828
|
+
// ★打った字と Enter の間は一拍おく(b69c1608:すぐ続く Enter は改行に取り込まれることがある)。
|
|
1829
|
+
d.pause(RELAY_SUBMIT_PAUSE_MS);
|
|
1830
|
+
d.enter();
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
for (let t = 0;; t += o.pollMs) {
|
|
1836
|
+
if (d.listSessions().some((n) => !before.has(n))) {
|
|
1837
|
+
// ★新しい会話ができた直後は入力欄が整っていない(RELAY_FRESH_SETTLE_MS の説明)。
|
|
1838
|
+
await d.sleep(RELAY_FRESH_SETTLE_MS);
|
|
1839
|
+
return d.ready();
|
|
1840
|
+
}
|
|
1841
|
+
if (t >= o.waitMs)
|
|
1842
|
+
return false;
|
|
1843
|
+
await d.sleep(o.pollMs);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1697
1846
|
/**
|
|
1698
1847
|
* 受け付けられるまで見て、未受付で入力欄に残っているときだけ Enter を押し直す。
|
|
1699
1848
|
* accepted … 押し直さずに受け付けられた
|
|
@@ -2641,6 +2790,19 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2641
2790
|
noteReadiness(state, 'not-agent', cmd.id);
|
|
2642
2791
|
return;
|
|
2643
2792
|
}
|
|
2793
|
+
// ── 1便1context(★既定オフ・便 87a1a28a) ──
|
|
2794
|
+
// 便を配る直前に職人の会話を新しくする(/clear)。★便を追っている間(state.pending)はここへ
|
|
2795
|
+
// 来ない=質問・確認待ちの便は同じ会話のまま続く。新しくできたと確かめられなければ配らない
|
|
2796
|
+
// (次の周でやり直す)。Codex の職人は対象外。
|
|
2797
|
+
if (cfg.freshContext && kindOf(cfg) === 'claude') {
|
|
2798
|
+
const cwd = tmuxPaneCwd(cfg.site);
|
|
2799
|
+
const fresh = cwd !== null && (await freshenContext(cfg.site, cwd));
|
|
2800
|
+
if (!fresh) {
|
|
2801
|
+
log(`WARN site=${cfg.site} id=${cmd.id} 会話を新しくできなかったので配らずに次の周でやり直します`);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
log(`site=${cfg.site} id=${cmd.id} 会話を新しくしました(1便1context)`);
|
|
2805
|
+
}
|
|
2644
2806
|
// ── tmux 送信 ──
|
|
2645
2807
|
// この送信に固有のアンカー(buildAnchor(id))を本文末尾に埋め込む。
|
|
2646
2808
|
// 画面にエコーされたアンカー行を「今回ジョブの境界」として記録し、完了検出は
|