@tyhld/conductor 0.12.66 → 0.12.67

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.
Files changed (2) hide show
  1. package/dist/headless.js +70 -20
  2. package/package.json +1 -1
package/dist/headless.js CHANGED
@@ -129,35 +129,51 @@ export function loadAndClearLegacyActiveJob() {
129
129
  }
130
130
  }
131
131
  /**
132
- * 手元の記録が自分(この中継)のものか照合する。
133
- * site, machine, workDir, gitOrigin を比べる。
134
- * 1点でも不一致なら理由を返す。すべて一致なら null。
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
- if (rec.site !== undefined && rec.site !== cfg.site) {
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
- if (rec.machine !== undefined && rec.machine !== cfg.machine) {
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
- if (rec.workDir !== undefined && rec.workDir !== cfg.workDir) {
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
- // ★git リポの同一性: パスが同じでも別のリポなら止める
151
- if (rec.gitOrigin !== undefined) {
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 !== null && rec.gitOrigin !== 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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -755,25 +771,38 @@ export async function restoreHeadlessOrphans(cfg, state) {
755
771
  log(`site=${cfg.site} 古い共有の active-job.json を消しました(現場ごとの記録を優先)`);
756
772
  }
757
773
  if (saved !== null) {
758
- // ★4点照合: machine, site, commandId, workDir がすべて一致しなければ再開しない
774
+ // ★所有者照合(Fail Closed): site, machine, workDir, gitOrigin がすべて確認でき一致しなければ再開しない
759
775
  const mismatch = validateActiveJobOwnership(saved, cfg);
760
776
  if (mismatch !== null) {
761
- log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録は別の中継のもの → 再開しない(不一致: ${mismatch})`);
777
+ log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録は自分のものと確認できない → 再開しない(${mismatch})`);
762
778
  await sendCommandEvents(cfg, saved.commandId, [{
763
779
  type: 'error',
764
- text: `手元の記録の4点照合で不一致 → 再開しない(${mismatch})`.slice(0, EVENT_TEXT_MAX),
780
+ text: `所有者照合で再開しない(${mismatch})`.slice(0, EVENT_TEXT_MAX),
765
781
  at: new Date().toISOString(),
766
782
  attempt: saved.attempt,
767
783
  }]);
768
784
  return;
769
785
  }
770
- // ★便IDの照合: 中央でまだ sent のまま残っている便か確かめる
786
+ // ★便IDの照合(Fail Closed): 中央でまだ sent のまま残っている便か確かめる。
787
+ // 確かめられなかった(null)ときも再開しない。ただし記録を戻して次の tick で再試行する。
771
788
  const centralStatus = await fetchCommandStatus(cfg, saved.commandId);
772
- if (centralStatus !== null && centralStatus !== 'sent') {
773
- log(`WARN site=${cfg.site} id=${saved.commandId} 手元の記録の便は中央で ${centralStatus} → 再開しない`);
789
+ if (centralStatus === null) {
790
+ log(`WARN site=${cfg.site} id=${saved.commandId} 中央の状態を確かめられない → 再開しない(次の tick で再試行)`);
791
+ // ★記録を戻す: 次の tick でもう一度確かめられるようにする(便を黙って捨てない)
792
+ saveActiveJob(cfg.site, saved);
793
+ await sendCommandEvents(cfg, saved.commandId, [{
794
+ type: 'error',
795
+ text: '中央の便の状態を確かめられない(ネットワーク障害等)→ 再開しない(次の tick で再試行)'.slice(0, EVENT_TEXT_MAX),
796
+ at: new Date().toISOString(),
797
+ attempt: saved.attempt,
798
+ }]);
799
+ return;
800
+ }
801
+ if (centralStatus !== 'sent') {
802
+ log(`WARN site=${cfg.site} id=${saved.commandId} 中央で ${centralStatus} → 再開しない`);
774
803
  await sendCommandEvents(cfg, saved.commandId, [{
775
804
  type: 'error',
776
- text: `便IDの照合: 中央の状態が sent ではない(${centralStatus})→ 再開しない`.slice(0, EVENT_TEXT_MAX),
805
+ text: `中央の便の状態が sent ではない(${centralStatus})→ 再開しない`.slice(0, EVENT_TEXT_MAX),
777
806
  at: new Date().toISOString(),
778
807
  attempt: saved.attempt,
779
808
  }]);
@@ -813,10 +842,31 @@ export async function restoreHeadlessOrphans(cfg, state) {
813
842
  return;
814
843
  }
815
844
  // ── 経路2: 中央から孤児を拾う(PC 再起動後) ──
845
+ // ★Fail Closed: 中央の孤児には machine/workDir/gitOrigin が無い。
846
+ // 手元の記録(active-job ファイル)が無い場合、所有者照合で確認不能 → 再開しない。
847
+ // 手元の記録があれば経路1で拾われているので、経路2に来る時点で手元の記録は無い。
816
848
  const orphans = await fetchOrphans(cfg);
817
849
  const pick = selectOrphanToRestore(orphans, null);
818
850
  if (pick === null)
819
851
  return;
852
+ // ★所有者照合(Fail Closed): 中央の孤児は所有者情報を持たないため確認不能。
853
+ // 手元の記録があれば経路1で処理済み。ここに来る=手元の記録が無い=確認不能。
854
+ const orphanAsRecord = {
855
+ commandId: pick.id, body: pick.body, sessionId: '', attempt: 0,
856
+ startedAt: 0, site: pick.site,
857
+ // machine, workDir, gitOrigin は中央に無い → undefined → 確認不能
858
+ };
859
+ const orphanMismatch = validateActiveJobOwnership(orphanAsRecord, cfg);
860
+ if (orphanMismatch !== null) {
861
+ log(`WARN site=${cfg.site} id=${pick.id} 中央の孤児は所有者を確認できない → 再開しない(${orphanMismatch})`);
862
+ await sendCommandEvents(cfg, pick.id, [{
863
+ type: 'error',
864
+ text: `中央の孤児の所有者照合で再開しない(${orphanMismatch})`.slice(0, EVENT_TEXT_MAX),
865
+ at: new Date().toISOString(),
866
+ attempt: 0,
867
+ }]);
868
+ return;
869
+ }
820
870
  const attempt = (state.attempts.get(pick.id) ?? 0) + 1;
821
871
  state.attempts.set(pick.id, attempt);
822
872
  if (attempt > HEADLESS_MAX_ATTEMPTS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.66",
3
+ "version": "0.12.67",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {