@tyhld/conductor 0.12.66 → 0.12.68
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 +80 -24
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFileSync, spawn } from 'node:child_process';
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import { RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_PENDING_TIMEOUT_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteReadiness, patchStatus, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
@@ -129,35 +129,51 @@ export function loadAndClearLegacyActiveJob() {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
-
*
|
|
133
|
-
* site, machine, workDir, gitOrigin
|
|
134
|
-
*
|
|
132
|
+
* 手元の記録が自分(この中継)のものか照合する(Fail Closed)。
|
|
133
|
+
* site, machine, workDir, gitOrigin の4項目を比べる。
|
|
134
|
+
*
|
|
135
|
+
* ★Fail Closed: 確認できない項目(記録に無い・今の値が取れない)は
|
|
136
|
+
* 不一致と同じ扱いにする。確認できた項目がすべて一致した場合だけ null を返す。
|
|
135
137
|
*
|
|
136
138
|
* ★commandId は「この便が中央でまだ sent か」の確認(ネットワーク照合)で別に行う。
|
|
137
139
|
* ここではローカルの所有者照合のみ。
|
|
138
140
|
*/
|
|
139
141
|
export function validateActiveJobOwnership(rec, cfg) {
|
|
140
142
|
const mismatches = [];
|
|
141
|
-
|
|
143
|
+
// site — 記録に無ければ確認不能
|
|
144
|
+
if (rec.site === undefined) {
|
|
145
|
+
mismatches.push('site: 記録に無い(確認不能)');
|
|
146
|
+
}
|
|
147
|
+
else if (rec.site !== cfg.site) {
|
|
142
148
|
mismatches.push(`site: 記録=${rec.site} 自分=${cfg.site}`);
|
|
143
149
|
}
|
|
144
|
-
|
|
150
|
+
// machine — 記録に無ければ確認不能
|
|
151
|
+
if (rec.machine === undefined) {
|
|
152
|
+
mismatches.push('machine: 記録に無い(確認不能)');
|
|
153
|
+
}
|
|
154
|
+
else if (rec.machine !== cfg.machine) {
|
|
145
155
|
mismatches.push(`machine: 記録=${rec.machine} 自分=${cfg.machine}`);
|
|
146
156
|
}
|
|
147
|
-
|
|
157
|
+
// workDir — 記録に無ければ確認不能
|
|
158
|
+
if (rec.workDir === undefined) {
|
|
159
|
+
mismatches.push('workDir: 記録に無い(確認不能)');
|
|
160
|
+
}
|
|
161
|
+
else if (rec.workDir !== cfg.workDir) {
|
|
148
162
|
mismatches.push(`workDir: 記録=${rec.workDir} 自分=${cfg.workDir}`);
|
|
149
163
|
}
|
|
150
|
-
//
|
|
151
|
-
if (rec.gitOrigin
|
|
164
|
+
// gitOrigin — 記録に無い or 今の値が取れない → 確認不能
|
|
165
|
+
if (rec.gitOrigin === undefined) {
|
|
166
|
+
mismatches.push('gitOrigin: 記録に無い(確認不能)');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
152
169
|
const currentOrigin = resolveGitOrigin(cfg.workDir);
|
|
153
|
-
if (currentOrigin
|
|
170
|
+
if (currentOrigin === null) {
|
|
171
|
+
mismatches.push('gitOrigin: 今のリポの origin を取得できない(確認不能)');
|
|
172
|
+
}
|
|
173
|
+
else if (rec.gitOrigin !== currentOrigin) {
|
|
154
174
|
mismatches.push(`gitOrigin: 記録=${rec.gitOrigin} 自分=${currentOrigin}`);
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
|
-
// 照合項目がそろわない古い記録は安全に拾えない。
|
|
158
|
-
if (rec.site === undefined || rec.machine === undefined || rec.workDir === undefined) {
|
|
159
|
-
mismatches.push('照合項目が不足(古い記録)');
|
|
160
|
-
}
|
|
161
177
|
return mismatches.length > 0 ? mismatches.join(', ') : null;
|
|
162
178
|
}
|
|
163
179
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -174,7 +190,6 @@ function claudeBinCandidates() {
|
|
|
174
190
|
// nvm の各 node 版の bin(新しい版から)
|
|
175
191
|
const nvmDir = path.join(h, '.nvm', 'versions', 'node');
|
|
176
192
|
try {
|
|
177
|
-
const { readdirSync } = require('node:fs');
|
|
178
193
|
const versions = readdirSync(nvmDir)
|
|
179
194
|
.filter((v) => v.startsWith('v'))
|
|
180
195
|
.sort((a, b) => {
|
|
@@ -191,8 +206,11 @@ function claudeBinCandidates() {
|
|
|
191
206
|
candidates.push(path.join(nvmDir, v, 'bin', 'claude'));
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
|
-
catch {
|
|
195
|
-
// nvm
|
|
209
|
+
catch (e) {
|
|
210
|
+
// nvm が無い(ENOENT)は正常。それ以外は探索の失敗として記録する。
|
|
211
|
+
if (e?.code !== 'ENOENT') {
|
|
212
|
+
console.error('[conductor] nvm claude exploration failed:', e);
|
|
213
|
+
}
|
|
196
214
|
}
|
|
197
215
|
// その他の既知の場所
|
|
198
216
|
candidates.push(path.join(h, '.local', 'bin', 'claude'), path.join(h, '.claude', 'local', 'claude'), '/usr/local/bin/claude', '/usr/bin/claude');
|
|
@@ -755,25 +773,38 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
755
773
|
log(`site=${cfg.site} 古い共有の active-job.json を消しました(現場ごとの記録を優先)`);
|
|
756
774
|
}
|
|
757
775
|
if (saved !== null) {
|
|
758
|
-
//
|
|
776
|
+
// ★所有者照合(Fail Closed): site, machine, workDir, gitOrigin がすべて確認でき一致しなければ再開しない
|
|
759
777
|
const mismatch = validateActiveJobOwnership(saved, cfg);
|
|
760
778
|
if (mismatch !== null) {
|
|
761
|
-
log(`WARN site=${cfg.site} id=${saved.commandId}
|
|
779
|
+
log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録は自分のものと確認できない → 再開しない(${mismatch})`);
|
|
762
780
|
await sendCommandEvents(cfg, saved.commandId, [{
|
|
763
781
|
type: 'error',
|
|
764
|
-
text:
|
|
782
|
+
text: `所有者照合で再開しない(${mismatch})`.slice(0, EVENT_TEXT_MAX),
|
|
765
783
|
at: new Date().toISOString(),
|
|
766
784
|
attempt: saved.attempt,
|
|
767
785
|
}]);
|
|
768
786
|
return;
|
|
769
787
|
}
|
|
770
|
-
// ★便ID
|
|
788
|
+
// ★便IDの照合(Fail Closed): 中央でまだ sent のまま残っている便か確かめる。
|
|
789
|
+
// 確かめられなかった(null)ときも再開しない。ただし記録を戻して次の tick で再試行する。
|
|
771
790
|
const centralStatus = await fetchCommandStatus(cfg, saved.commandId);
|
|
772
|
-
if (centralStatus
|
|
773
|
-
log(`WARN site=${cfg.site} id=${saved.commandId}
|
|
791
|
+
if (centralStatus === null) {
|
|
792
|
+
log(`WARN site=${cfg.site} id=${saved.commandId} 中央の状態を確かめられない → 再開しない(次の tick で再試行)`);
|
|
793
|
+
// ★記録を戻す: 次の tick でもう一度確かめられるようにする(便を黙って捨てない)
|
|
794
|
+
saveActiveJob(cfg.site, saved);
|
|
795
|
+
await sendCommandEvents(cfg, saved.commandId, [{
|
|
796
|
+
type: 'error',
|
|
797
|
+
text: '中央の便の状態を確かめられない(ネットワーク障害等)→ 再開しない(次の tick で再試行)'.slice(0, EVENT_TEXT_MAX),
|
|
798
|
+
at: new Date().toISOString(),
|
|
799
|
+
attempt: saved.attempt,
|
|
800
|
+
}]);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (centralStatus !== 'sent') {
|
|
804
|
+
log(`WARN site=${cfg.site} id=${saved.commandId} 中央で ${centralStatus} → 再開しない`);
|
|
774
805
|
await sendCommandEvents(cfg, saved.commandId, [{
|
|
775
806
|
type: 'error',
|
|
776
|
-
text:
|
|
807
|
+
text: `中央の便の状態が sent ではない(${centralStatus})→ 再開しない`.slice(0, EVENT_TEXT_MAX),
|
|
777
808
|
at: new Date().toISOString(),
|
|
778
809
|
attempt: saved.attempt,
|
|
779
810
|
}]);
|
|
@@ -794,6 +825,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
794
825
|
const cmd = { id: pick.id, body: pick.body };
|
|
795
826
|
const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, true);
|
|
796
827
|
if (job === null) {
|
|
828
|
+
await patchStatus(cfg, pick.id, 'failed', 'claude が見つかりません(手元の孤児復元を中止)');
|
|
797
829
|
state.relay.abandoned.add(pick.id);
|
|
798
830
|
log(`ERROR site=${cfg.site} id=${pick.id} 手元の孤児復元: claude が見つからない → 放棄`);
|
|
799
831
|
return;
|
|
@@ -813,10 +845,31 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
813
845
|
return;
|
|
814
846
|
}
|
|
815
847
|
// ── 経路2: 中央から孤児を拾う(PC 再起動後) ──
|
|
848
|
+
// ★Fail Closed: 中央の孤児には machine/workDir/gitOrigin が無い。
|
|
849
|
+
// 手元の記録(active-job ファイル)が無い場合、所有者照合で確認不能 → 再開しない。
|
|
850
|
+
// 手元の記録があれば経路1で拾われているので、経路2に来る時点で手元の記録は無い。
|
|
816
851
|
const orphans = await fetchOrphans(cfg);
|
|
817
852
|
const pick = selectOrphanToRestore(orphans, null);
|
|
818
853
|
if (pick === null)
|
|
819
854
|
return;
|
|
855
|
+
// ★所有者照合(Fail Closed): 中央の孤児は所有者情報を持たないため確認不能。
|
|
856
|
+
// 手元の記録があれば経路1で処理済み。ここに来る=手元の記録が無い=確認不能。
|
|
857
|
+
const orphanAsRecord = {
|
|
858
|
+
commandId: pick.id, body: pick.body, sessionId: '', attempt: 0,
|
|
859
|
+
startedAt: 0, site: pick.site,
|
|
860
|
+
// machine, workDir, gitOrigin は中央に無い → undefined → 確認不能
|
|
861
|
+
};
|
|
862
|
+
const orphanMismatch = validateActiveJobOwnership(orphanAsRecord, cfg);
|
|
863
|
+
if (orphanMismatch !== null) {
|
|
864
|
+
log(`WARN site=${cfg.site} id=${pick.id} 中央の孤児は所有者を確認できない → 再開しない(${orphanMismatch})`);
|
|
865
|
+
await sendCommandEvents(cfg, pick.id, [{
|
|
866
|
+
type: 'error',
|
|
867
|
+
text: `中央の孤児の所有者照合で再開しない(${orphanMismatch})`.slice(0, EVENT_TEXT_MAX),
|
|
868
|
+
at: new Date().toISOString(),
|
|
869
|
+
attempt: 0,
|
|
870
|
+
}]);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
820
873
|
const attempt = (state.attempts.get(pick.id) ?? 0) + 1;
|
|
821
874
|
state.attempts.set(pick.id, attempt);
|
|
822
875
|
if (attempt > HEADLESS_MAX_ATTEMPTS) {
|
|
@@ -830,6 +883,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
830
883
|
const cmd = { id: pick.id, body: pick.body };
|
|
831
884
|
const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, true);
|
|
832
885
|
if (job === null) {
|
|
886
|
+
await patchStatus(cfg, pick.id, 'failed', 'claude が見つかりません(孤児復元を中止)');
|
|
833
887
|
await sendCommandEvents(cfg, pick.id, [{
|
|
834
888
|
type: 'error',
|
|
835
889
|
text: 'claude が見つかりません(孤児復元を中止)',
|
|
@@ -923,6 +977,7 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
923
977
|
: sessionIdForCommand(retry.cmd.id, retry.attempt);
|
|
924
978
|
const job = spawnClaudeProcess(cfg, retry.cmd, sessionId, retry.attempt, isResume);
|
|
925
979
|
if (job === null) {
|
|
980
|
+
await patchStatus(cfg, retry.cmd.id, 'failed', 'claude が見つかりません(自動再開を中止)');
|
|
926
981
|
await sendCommandEvents(cfg, retry.cmd.id, [{
|
|
927
982
|
type: 'error',
|
|
928
983
|
text: 'claude が見つかりません(自動再開を中止)',
|
|
@@ -979,6 +1034,7 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
979
1034
|
const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume);
|
|
980
1035
|
if (job === null) {
|
|
981
1036
|
// ★起動失敗を管制へ知らせる(「起動しました」とは言わない)
|
|
1037
|
+
await patchStatus(cfg, cmd.id, 'failed', 'claude が見つかりません(起動できませんでした)');
|
|
982
1038
|
await sendCommandEvents(cfg, cmd.id, [{
|
|
983
1039
|
type: 'error',
|
|
984
1040
|
text: 'claude が見つかりません(起動できませんでした)',
|