@volter/supercode-orchestrator 0.4.119 → 0.4.120
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/board/dispatch.mjs +68 -22
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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',
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
911
|
-
const r = sc(['teams', 'panes', 'ls',
|
|
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
|
|
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
|
|
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',
|
|
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
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.120",
|
|
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": {
|