@volter/supercode-orchestrator 0.4.119 → 0.4.121

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.
@@ -201,27 +201,39 @@ export function lanesOf(profiles = {}) {
201
201
  return out;
202
202
  }
203
203
 
204
+ const onMachine = machine => normalMachine(machine ?? localMachine()) === localMachine() ? [] : ['--machine', machine];
205
+ const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
206
+
204
207
  // ------------------------------------------------------------------ supervise
205
208
 
206
209
  function supervise(board, fleet, settings, report, entry, effects) {
207
210
  const rows = board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all();
208
211
  // The catalog can lag a pane. Read the actual machines once, before any board mutation.
209
212
  const observed = new Map();
213
+ const exits = new Map();
210
214
  for (const { id } of rows) {
211
215
  const s = currentRun(board.db, id)?.metadata?.supercode;
212
216
  if (!s?.pane || observed.has(s.machine)) continue;
213
- const r = sc(['teams', 'panes', 'ls', '--machine', s.machine ?? localMachine()], { timeout: 60_000 });
217
+ const r = sc(['teams', 'panes', 'ls', ...onMachine(s.machine)], { timeout: 60_000 });
214
218
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
215
- observed.set(s.machine, Array.isArray(list) ? new Set(list.map((p) => p.pane)) : null);
219
+ observed.set(s.machine, Array.isArray(list) ? new Set(list.filter(p => !p.dead).map((p) => p.pane)) : null);
216
220
  if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
217
221
  }
222
+ for (const {id} of rows) {
223
+ const session=currentRun(board.db,id)?.metadata?.supercode;
224
+ const panes=observed.get(session?.machine);
225
+ if(session?.launch_key && panes instanceof Set && !panes.has(session.pane)) {
226
+ try { const receipt=launchReceipt(board,session);if(receipt?.live===false&&receipt.exit)exits.set(id,receipt.exit); }
227
+ catch(error){report.failed.push([id,`exit receipt unavailable; retaining liveness observation: ${error.message}`]);}
228
+ }
229
+ }
218
230
  for (const { id } of rows) {
219
231
  tx(board, (db) => {
220
232
  const task = getTask(db, id);
221
233
  if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
222
234
  const runRow = currentRun(db, id);
223
235
  let session = runRow?.metadata?.supercode ?? null;
224
- if (session?.launch_pending) {
236
+ if (session?.launch_pending || session?.launch_deferred) {
225
237
  extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
226
238
  return; // An uncertain launch owns this run until a machine receipt settles it.
227
239
  }
@@ -305,6 +317,11 @@ function supervise(board, fleet, settings, report, entry, effects) {
305
317
  return;
306
318
  }
307
319
  if (session?.session_id) {
320
+ if(exits.has(id)) {
321
+ const exit=exits.get(id);
322
+ observe('exited',{code:exit.failed?1:0,error:`native session ${session.session_id} ended: ${exit.how}`,session_id:session.session_id,pane:session.pane},'crashed');
323
+ return;
324
+ }
308
325
  if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
309
326
  if (!session.pane && !fleet.ok) return;
310
327
  const alive = session.pane ? paneLive : sessionAlive(fleet, session);
@@ -521,9 +538,23 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
521
538
  if (slot === 'worker' || (worker?.session_id && worker.session_id === reviewer?.session_id)) throw new Error('a reviewer must have a different session from the implementer');
522
539
  }
523
540
  const meta = { status, role: spec.role, slot };
524
- return surface === 'pane'
525
- ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
526
- : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
541
+ try {
542
+ const result = surface === 'pane'
543
+ ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
544
+ : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
545
+ tx(board,db=>setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null}));
546
+ return result;
547
+ } catch(error) {
548
+ if(error.launchDeferred) {
549
+ tx(board,db=>{
550
+ const previous=currentRun(db,id)?.metadata?.supercode;
551
+ setRunSession(db,runId,{...meta,launch_deferred:true,launch_deferred_error:error.message});
552
+ if(previous?.launch_deferred_error!==error.message)event(db,id,'launch_deferred',{reason:error.message},runId);
553
+ });
554
+ error.launchPending=true;
555
+ }
556
+ throw error;
557
+ }
527
558
  }
528
559
 
529
560
  /** Start one card now by a person's word (a triage card's session, a manager's start): the workflow's start event. */
@@ -589,9 +620,9 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
589
620
  try {
590
621
  const machine = session.machine ?? localMachine();
591
622
  if (!panes.has(machine)) {
592
- const r = sc(['teams', 'panes', 'ls', '--machine', machine], { timeout: 60_000, as: boardAddress(board.label) });
623
+ const r = sc(['teams', 'panes', 'ls', ...onMachine(machine)], { timeout: 60_000, as: boardAddress(board.label) });
593
624
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
594
- panes.set(machine, Array.isArray(list) ? { live: new Set(list.map((p) => p.pane)) } : { error: `cannot read panes on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}` });
625
+ panes.set(machine, Array.isArray(list) ? { live: new Set(list.filter(p => !p.dead).map((p) => p.pane)) } : { error: `cannot read panes on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}` });
595
626
  }
596
627
  const observed = panes.get(machine);
597
628
  if (observed.error) throw new Error(observed.error);
@@ -801,12 +832,12 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
801
832
 
802
833
  /** A transport error is not a negative launch receipt. The key remains owned by its run. */
803
834
  function launchReceipt(board, session) {
804
- const r = sc(['teams', 'launches', 'get', session.launch_key, '--machine', session.machine],
835
+ const r = sc(['teams', 'launches', 'get', session.launch_key, ...onMachine(session.machine)],
805
836
  { timeout: 30_000, as: boardAddress(board.label) });
806
- if (r.code !== 0) throw new Error(`launch receipt unavailable on ${session.machine}: install the matching Teams CLI and machine capability (teams launches get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
837
+ if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: install the matching Teams CLI and machine capability (teams launches get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
807
838
  if (r.stdout.trim() === 'null') return null;
808
839
  const receipt = lastJson(r.stdout);
809
- if (!receipt || receipt.key !== session.launch_key) throw new Error(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
840
+ if (!receipt || receipt.key !== session.launch_key) throw unavailable(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
810
841
  return receipt;
811
842
  }
812
843
 
@@ -854,7 +885,22 @@ export function reconcileCallerLaunch(board, sid) {
854
885
  }
855
886
  }
856
887
 
857
- function reconcileLaunches(board, fleet, report, entry, effects, settings) {
888
+ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
889
+ const deferred=view(board,db=>db.prepare("SELECT t.id FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
890
+ for(const {id} of deferred) {
891
+ try {
892
+ const task=getTaskNow(board,id),status=view(board,db=>statusOf(db,task)),spec=runSpec(entry,status,task);
893
+ const profile=spec&&view(board,db=>evaluate(entry.workflow.roles?.[spec.role]??'card.assignee',scopeOf(db,board,task,{params:entry.params})));
894
+ const lane=lanes?.[profile];if(!lane){report.queued.push([id,'launch waits for its configured lane']);continue;}
895
+ const why=launcherCheck(board.db,task,lane,fleet);if(why){report.queued.push([id,why]);continue;}
896
+ const session=launchRun(board,entry,id,lane,fleet,settings);
897
+ if(session)report.started.push([id,session.address??session.session_id,session.mode]);
898
+ }catch(error){
899
+ if(error.launchPending){report.queued.push([id,error.message]);continue;}
900
+ const moved=tx(board,db=>fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
901
+ report.failed.push([id,error.message]);
902
+ }
903
+ }
858
904
  const rows = view(board, (db) => db.prepare(`SELECT id, task_id, ended_at, metadata FROM task_runs
859
905
  WHERE json_extract(metadata, '$.supercode.launch_pending') = 1
860
906
  AND json_extract(metadata, '$.supercode.close_confirmed_at') IS NULL`).all());
@@ -903,15 +949,15 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
903
949
  const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
904
950
  .map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
905
951
  .find((r) => (r.session?.pane || r.session?.launch_key) && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
906
- if (pending) throw new Error(`slot ${slot} still owns pane ${pending.session.pane} from run ${pending.id}; closure must be confirmed before another session starts: ${pending.session.close_error ?? 'close pending'}`);
952
+ if (pending) throw unavailable(`slot ${slot} still owns pane ${pending.session.pane} from run ${pending.id}; closure must be confirmed before another session starts: ${pending.session.close_error ?? 'close pending'}`);
907
953
  let prev = view(board, (db) => lastSession(db, task.id, slot));
908
954
  if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
909
955
  const fresh = observeFleet();
910
- if (!fresh.ok) throw new Error(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
911
- const r = sc(['teams', 'panes', 'ls', '--machine', machine], { timeout: 60_000 });
956
+ if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
957
+ const r = sc(['teams', 'panes', 'ls', ...onMachine(machine)], { timeout: 60_000 });
912
958
  const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
913
- if (!Array.isArray(listed)) throw new Error(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
914
- const pane = continuationPane(fresh, prev, new Set(listed.map((p) => p.pane)));
959
+ if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
960
+ const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
915
961
  if (pane && pane !== prev.pane) prev = { ...prev, pane, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
916
962
  fleet = fresh;
917
963
  }
@@ -920,7 +966,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
920
966
  closeRequested(board, report, { taskId: task.id });
921
967
  prev = view(board, (db) => lastSession(db, task.id, slot));
922
968
  // a pane that could not be closed still holds the session: no second process is started on it
923
- if (prev?.close_requested && !prev.closed_at) throw new Error(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
969
+ if (prev?.close_requested && !prev.closed_at) throw unavailable(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
924
970
  }
925
971
  const openOwned = (key, sid, mode, args) => {
926
972
  launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
@@ -1035,10 +1081,10 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1035
1081
 
1036
1082
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1037
1083
  function panePresent(machine, pane) {
1038
- const r = sc(['teams', 'panes', 'ls', '--machine', machine ?? localMachine()], { timeout: 60_000 });
1084
+ const r = sc(['teams', 'panes', 'ls', ...onMachine(machine)], { timeout: 60_000 });
1039
1085
  const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
1040
- if (!Array.isArray(panes)) throw new Error(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1041
- return panes.some((p) => p.pane === pane);
1086
+ if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1087
+ return panes.some((p) => p.pane === pane && !p.dead);
1042
1088
  }
1043
1089
 
1044
1090
  /** Whether a pane just launched is still there after the harness had a moment to start. */
@@ -1576,7 +1622,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
1576
1622
  readMailbox(board, boardAddress(board.label), report); phase('mailbox');
1577
1623
  await refreshRelationAnswers(board); phase('answers');
1578
1624
  const effects = [];
1579
- reconcileLaunches(board, fleet, report, entry, effects, settings); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
1625
+ reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
1580
1626
  report.unsettled = [];
1581
1627
  report.promoted = recomputeReady(board, effects, report.unsettled);
1582
1628
  await sendBoardMessages(board, report);
package/board/ztrack.mjs CHANGED
@@ -268,8 +268,8 @@ class ZtrackBoard {
268
268
  load({ observe = false } = {}) {
269
269
  const text = read(this.path), archiveText = read(this.archive);
270
270
  const saved=this.get('snapshot', {});
271
- let cached=saved.documentHashes?.open===hash(text)&&saved.documentHashes?.archived===hash(archiveText);
272
- if(!observe && !cached && saved.documentHashes) {
271
+ let cached=saved.bodyProjection===2&&saved.documentHashes?.open===hash(text)&&saved.documentHashes?.archived===hash(archiveText);
272
+ if(!observe && !cached && saved.bodyProjection===2 && saved.documentHashes) {
273
273
  // The durable intent may have replaced the documents while its runtime commit
274
274
  // is still pending. Readers keep the previous committed projection, never parse
275
275
  // an intermediate pair of files. A real hand edit does not match these hashes.
@@ -495,7 +495,7 @@ class ZtrackBoard {
495
495
  if (this.saved.documentHashes?.open === hash(this.text) && this.saved.documentHashes?.archived === hash(this.archiveText)) {
496
496
  for (const id of Object.keys(fingerprints)) if (this.saved.fingerprints?.[id]) fingerprints[id] = this.saved.fingerprints[id];
497
497
  }
498
- return { documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: { ...fingerprints, ...this.publishedFingerprints }, taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
498
+ return { bodyProjection:2, documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: { ...fingerprints, ...this.publishedFingerprints }, taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
499
499
  }
500
500
 
501
501
  lift() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.119",
3
+ "version": "0.4.121",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {