@tyhld/conductor 0.12.62 → 0.12.63
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/headless.js +95 -19
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -42,33 +42,43 @@ const RESUME_PROMPT = '前回の作業の続きを行ってください。中断
|
|
|
42
42
|
export const RUNS_DIR = path.join(os.homedir(), '.local', 'state', 'conductor', 'runs');
|
|
43
43
|
/** セッションありと見なす最低実行時間(ms)。これより短いとセッションファイルが無い可能性が高い。 */
|
|
44
44
|
export const SESSION_MIN_DURATION_MS = 30_000;
|
|
45
|
-
/**
|
|
46
|
-
export const
|
|
45
|
+
/** 実行中のジョブを永続化するディレクトリ。中継再起動後の拾い直しに使う。 */
|
|
46
|
+
export const ACTIVE_JOB_DIR = path.join(os.homedir(), '.local', 'state', 'conductor');
|
|
47
|
+
/**
|
|
48
|
+
* 旧: PCで1つの共有パス(0.12.62 以前)。
|
|
49
|
+
* 現場ごとに分けたあとも、古い記録が残っている PC で誤って拾わないように参照する。
|
|
50
|
+
*/
|
|
51
|
+
export const ACTIVE_JOB_PATH_LEGACY = path.join(ACTIVE_JOB_DIR, 'active-job.json');
|
|
52
|
+
/** 現場ごとの記録ファイルのパスを返す。 */
|
|
53
|
+
export function activeJobPath(site) {
|
|
54
|
+
return path.join(ACTIVE_JOB_DIR, `active-job-${site}.json`);
|
|
55
|
+
}
|
|
47
56
|
/** 実行中のジョブをファイルに書く(中継が死んでも拾い直せるように)。 */
|
|
48
|
-
export function saveActiveJob(record) {
|
|
57
|
+
export function saveActiveJob(site, record) {
|
|
49
58
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
writeFileSync(ACTIVE_JOB_PATH, JSON.stringify(record) + '\n');
|
|
59
|
+
mkdirSync(ACTIVE_JOB_DIR, { recursive: true });
|
|
60
|
+
writeFileSync(activeJobPath(site), JSON.stringify(record) + '\n');
|
|
53
61
|
}
|
|
54
62
|
catch (err) {
|
|
55
63
|
log(`WARN active-job の保存に失敗: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
/** 実行中のジョブ記録を消す(ジョブが完了・放棄したとき)。 */
|
|
59
|
-
export function clearActiveJob() {
|
|
67
|
+
export function clearActiveJob(site) {
|
|
60
68
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const p = activeJobPath(site);
|
|
70
|
+
if (existsSync(p))
|
|
71
|
+
unlinkSync(p);
|
|
63
72
|
}
|
|
64
73
|
catch { /* ignore */ }
|
|
65
74
|
}
|
|
66
75
|
/** 起動時に前回の実行中ジョブを読む。無ければ null。 */
|
|
67
|
-
export function loadActiveJob() {
|
|
76
|
+
export function loadActiveJob(site) {
|
|
68
77
|
try {
|
|
69
|
-
|
|
78
|
+
const p = activeJobPath(site);
|
|
79
|
+
if (!existsSync(p))
|
|
70
80
|
return null;
|
|
71
|
-
const raw = readFileSync(
|
|
81
|
+
const raw = readFileSync(p, 'utf8').trim();
|
|
72
82
|
if (!raw)
|
|
73
83
|
return null;
|
|
74
84
|
const rec = JSON.parse(raw);
|
|
@@ -80,6 +90,49 @@ export function loadActiveJob() {
|
|
|
80
90
|
return null;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 旧形式(PCで1つの共有 active-job.json)を読み、在れば消す。
|
|
95
|
+
* 4点がそろわない古い記録は再開に使わない。
|
|
96
|
+
*/
|
|
97
|
+
export function loadAndClearLegacyActiveJob() {
|
|
98
|
+
try {
|
|
99
|
+
if (!existsSync(ACTIVE_JOB_PATH_LEGACY))
|
|
100
|
+
return null;
|
|
101
|
+
const raw = readFileSync(ACTIVE_JOB_PATH_LEGACY, 'utf8').trim();
|
|
102
|
+
unlinkSync(ACTIVE_JOB_PATH_LEGACY); // 読んだら必ず消す
|
|
103
|
+
if (!raw)
|
|
104
|
+
return null;
|
|
105
|
+
const rec = JSON.parse(raw);
|
|
106
|
+
if (!rec.commandId)
|
|
107
|
+
return null;
|
|
108
|
+
return rec;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 手元の記録が自分(この中継)のものか4点照合する。
|
|
116
|
+
* 1点でも不一致なら理由を返す。すべて一致なら null。
|
|
117
|
+
*/
|
|
118
|
+
export function validateActiveJobOwnership(rec, cfg) {
|
|
119
|
+
const mismatches = [];
|
|
120
|
+
if (rec.site !== undefined && rec.site !== cfg.site) {
|
|
121
|
+
mismatches.push(`site: 記録=${rec.site} 自分=${cfg.site}`);
|
|
122
|
+
}
|
|
123
|
+
if (rec.machine !== undefined && rec.machine !== cfg.machine) {
|
|
124
|
+
mismatches.push(`machine: 記録=${rec.machine} 自分=${cfg.machine}`);
|
|
125
|
+
}
|
|
126
|
+
if (rec.workDir !== undefined && rec.workDir !== cfg.workDir) {
|
|
127
|
+
mismatches.push(`workDir: 記録=${rec.workDir} 自分=${cfg.workDir}`);
|
|
128
|
+
}
|
|
129
|
+
// 4点がそろわない古い記録(0.12.62 以前)は site/machine/workDir が undefined。
|
|
130
|
+
// 照合できない = 安全に拾えない。
|
|
131
|
+
if (rec.site === undefined || rec.machine === undefined || rec.workDir === undefined) {
|
|
132
|
+
mismatches.push('4点の照合項目が不足(0.12.62 以前の古い記録)');
|
|
133
|
+
}
|
|
134
|
+
return mismatches.length > 0 ? mismatches.join(', ') : null;
|
|
135
|
+
}
|
|
83
136
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
137
|
// claude の実行ファイルの探索
|
|
85
138
|
//
|
|
@@ -479,7 +532,7 @@ async function checkRunningJob(cfg, state) {
|
|
|
479
532
|
// ── 完了したプロセスの後始末 ──
|
|
480
533
|
if (job.completed) {
|
|
481
534
|
await flushEvents(cfg, job);
|
|
482
|
-
clearActiveJob();
|
|
535
|
+
clearActiveJob(cfg.site);
|
|
483
536
|
if (job.stopped) {
|
|
484
537
|
// stop_requested で止めた
|
|
485
538
|
await sendCommandEvents(cfg, job.commandId, [{
|
|
@@ -610,9 +663,32 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
610
663
|
if (state.job !== null)
|
|
611
664
|
return; // 既にジョブ実行中
|
|
612
665
|
// ── 経路1: 手元の記録から拾い直す ──
|
|
613
|
-
|
|
666
|
+
// ★現場ごとのファイルを先に読む。古い共有ファイルも消す(取り違え防止)。
|
|
667
|
+
let saved = loadActiveJob(cfg.site);
|
|
614
668
|
if (saved !== null) {
|
|
615
|
-
clearActiveJob(); // 拾ったら消す(二重起動防止)
|
|
669
|
+
clearActiveJob(cfg.site); // 拾ったら消す(二重起動防止)
|
|
670
|
+
}
|
|
671
|
+
// 古い共有の記録(0.12.62 以前)が残っていれば読んで必ず消す
|
|
672
|
+
const legacy = loadAndClearLegacyActiveJob();
|
|
673
|
+
if (saved === null && legacy !== null) {
|
|
674
|
+
saved = legacy; // 新しい現場ごとの記録が無いときだけ候補にする
|
|
675
|
+
}
|
|
676
|
+
else if (legacy !== null) {
|
|
677
|
+
log(`site=${cfg.site} 古い共有の active-job.json を消しました(現場ごとの記録を優先)`);
|
|
678
|
+
}
|
|
679
|
+
if (saved !== null) {
|
|
680
|
+
// ★4点照合: machine, site, commandId, workDir がすべて一致しなければ再開しない
|
|
681
|
+
const mismatch = validateActiveJobOwnership(saved, cfg);
|
|
682
|
+
if (mismatch !== null) {
|
|
683
|
+
log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録は別の中継のもの → 再開しない(不一致: ${mismatch})`);
|
|
684
|
+
await sendCommandEvents(cfg, saved.commandId, [{
|
|
685
|
+
type: 'error',
|
|
686
|
+
text: `手元の記録の4点照合で不一致 → 再開しない(${mismatch})`.slice(0, EVENT_TEXT_MAX),
|
|
687
|
+
at: new Date().toISOString(),
|
|
688
|
+
attempt: saved.attempt,
|
|
689
|
+
}]);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
616
692
|
log(`site=${cfg.site} id=${saved.commandId} 手元の記録から孤児を検出(中継再起動後の拾い直し)`);
|
|
617
693
|
const pick = { id: saved.commandId, body: saved.body, site: cfg.site, sentAt: saved.startedAt };
|
|
618
694
|
const attempt = saved.attempt + 1;
|
|
@@ -633,7 +709,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
633
709
|
return;
|
|
634
710
|
}
|
|
635
711
|
state.job = job;
|
|
636
|
-
saveActiveJob({ commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now() });
|
|
712
|
+
saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
|
|
637
713
|
markDelivered(state.relay, pick.id);
|
|
638
714
|
state.relay.lastHeartbeatAt = null;
|
|
639
715
|
noteReadiness(state.relay, 'delivered', pick.id);
|
|
@@ -675,7 +751,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
675
751
|
return;
|
|
676
752
|
}
|
|
677
753
|
state.job = job;
|
|
678
|
-
saveActiveJob({ commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now() });
|
|
754
|
+
saveActiveJob(cfg.site, { commandId: pick.id, body: pick.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
|
|
679
755
|
markDelivered(state.relay, pick.id);
|
|
680
756
|
state.relay.lastHeartbeatAt = null;
|
|
681
757
|
noteReadiness(state.relay, 'delivered', pick.id);
|
|
@@ -768,7 +844,7 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
768
844
|
return;
|
|
769
845
|
}
|
|
770
846
|
state.job = job;
|
|
771
|
-
saveActiveJob({ commandId: retry.cmd.id, body: retry.cmd.body, sessionId, attempt: retry.attempt, startedAt: Date.now() });
|
|
847
|
+
saveActiveJob(cfg.site, { commandId: retry.cmd.id, body: retry.cmd.body, sessionId, attempt: retry.attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
|
|
772
848
|
rs.lastHeartbeatAt = null;
|
|
773
849
|
noteReadiness(rs, 'delivered', retry.cmd.id);
|
|
774
850
|
await sendCommandEvents(cfg, retry.cmd.id, [{
|
|
@@ -824,7 +900,7 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
824
900
|
return;
|
|
825
901
|
}
|
|
826
902
|
state.job = job;
|
|
827
|
-
saveActiveJob({ commandId: cmd.id, body: cmd.body, sessionId, attempt, startedAt: Date.now() });
|
|
903
|
+
saveActiveJob(cfg.site, { commandId: cmd.id, body: cmd.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir });
|
|
828
904
|
markDelivered(rs, cmd.id);
|
|
829
905
|
rs.lastHeartbeatAt = null;
|
|
830
906
|
noteReadiness(rs, 'delivered', cmd.id);
|