@volter/supercode-orchestrator 0.4.103 → 0.4.105
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 +43 -13
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -176,6 +176,16 @@ export function lanesOf(profiles = {}) {
|
|
|
176
176
|
|
|
177
177
|
function supervise(board, fleet, settings, report, entry, effects) {
|
|
178
178
|
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();
|
|
179
|
+
// The catalog can lag a pane. Read the actual machines once, before any board mutation.
|
|
180
|
+
const observed = new Map();
|
|
181
|
+
for (const { id } of rows) {
|
|
182
|
+
const s = currentRun(board.db, id)?.metadata?.supercode;
|
|
183
|
+
if (!s?.pane || observed.has(s.machine)) continue;
|
|
184
|
+
const r = sc(['teams', 'panes', 'ls', '--machine', s.machine ?? localMachine()], { timeout: 60_000 });
|
|
185
|
+
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
186
|
+
observed.set(s.machine, Array.isArray(list) ? new Set(list.map((p) => p.pane)) : null);
|
|
187
|
+
if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
|
|
188
|
+
}
|
|
179
189
|
for (const { id } of rows) {
|
|
180
190
|
tx(board, (db) => {
|
|
181
191
|
const task = getTask(db, id);
|
|
@@ -197,7 +207,9 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
197
207
|
};
|
|
198
208
|
const limits = limitsOf(db, board, entry, task, session);
|
|
199
209
|
// Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
|
|
200
|
-
const
|
|
210
|
+
const paneReading = session?.pane ? observed.get(session.machine) : undefined;
|
|
211
|
+
const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
|
|
212
|
+
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) : true) : false;
|
|
201
213
|
// (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
|
|
202
214
|
if ((!task.claim_lock || !task.claim_expires) && !live) {
|
|
203
215
|
observe('orphaned', { reason: 'orphaned_running', claim_lock: task.claim_lock, claim_expires: task.claim_expires, error: 'orphaned running card (broken claim bookkeeping)' }, 'reclaimed');
|
|
@@ -226,20 +238,21 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
226
238
|
return;
|
|
227
239
|
}
|
|
228
240
|
if (session?.session_id) {
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
const
|
|
241
|
+
if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
|
|
242
|
+
if (!session.pane && !fleet.ok) return;
|
|
243
|
+
const alive = session.pane ? paneLive : sessionAlive(fleet, session);
|
|
244
|
+
const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
|
|
232
245
|
const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
|
|
233
246
|
const missing = !alive && machineUp && !young;
|
|
234
247
|
const next = { ...session };
|
|
235
248
|
// a machine that is off the fleet: how long, for the workflow's machine_offline (the claim is kept meanwhile)
|
|
236
|
-
if (machineUp)
|
|
249
|
+
if (machineUp) next.offline_since = null; else next.offline_since ??= t;
|
|
237
250
|
if (!machineUp) {
|
|
238
251
|
setRunSession(db, runRow.id, next);
|
|
239
252
|
observe('machine_offline', { machine: session.machine, seconds: t - next.offline_since }, null);
|
|
240
253
|
if (getTask(db, id)?.current_run_id !== runRow.id) return; // the workflow moved it
|
|
241
254
|
}
|
|
242
|
-
if (alive) { next.seen_at ??= t;
|
|
255
|
+
if (alive) { next.seen_at ??= t; next.missing_since = null; }
|
|
243
256
|
if (missing) next.missing_since ??= t;
|
|
244
257
|
if (missing && t - next.missing_since >= Number(limits.grace ?? settings.grace)) {
|
|
245
258
|
// lost: gone from an online machine for the whole grace. A resume that never came up is not resumable.
|
|
@@ -500,7 +513,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
500
513
|
if (current.close_error !== error) event(db, row.task_id, 'session_close_failed', { session_id: session.session_id, pane: session.pane, error, retry: true }, row.id);
|
|
501
514
|
} else {
|
|
502
515
|
const { close_error: _error, ...rest } = current;
|
|
503
|
-
setRunSession(db, row.id, { ...rest, closed_at: at, close_confirmed_at: at });
|
|
516
|
+
setRunSession(db, row.id, { ...rest, close_error: null, closed_at: at, close_confirmed_at: at });
|
|
504
517
|
const address = addressOf(session);
|
|
505
518
|
const subscribed = active.some((r) => addressOf(json(r.metadata, {})?.supercode) === address && r.id !== row.id);
|
|
506
519
|
if (!subscribed) db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(row.task_id, address);
|
|
@@ -651,6 +664,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
651
664
|
// the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
|
|
652
665
|
// a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
|
|
653
666
|
// session starts (never two processes on one session)
|
|
667
|
+
// Every old pane in the slot must be settled, including a failed launch with no session id.
|
|
668
|
+
closeRequested(board, { undelivered: [] }, { taskId: task.id });
|
|
669
|
+
const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
|
|
670
|
+
.map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
|
|
671
|
+
.find((r) => r.session?.pane && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
|
|
672
|
+
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'}`);
|
|
654
673
|
let prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
655
674
|
if (prev?.close_requested && !prev.closed_at) {
|
|
656
675
|
const report = { undelivered: [] };
|
|
@@ -671,7 +690,10 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
671
690
|
};
|
|
672
691
|
let session = null;
|
|
673
692
|
let replaced = null;
|
|
674
|
-
if (prev?.forgotten)
|
|
693
|
+
if (prev?.forgotten) {
|
|
694
|
+
if (prev.pane && panePresent(prev.machine, prev.pane)) throw new Error(`forgotten session ${prev.session_id} still has live pane ${prev.pane}; close it before replacement`);
|
|
695
|
+
replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
|
|
696
|
+
}
|
|
675
697
|
else if (prev && (prev.harness ?? 'claude-code') !== harness) {
|
|
676
698
|
// the card was reassigned to a lane of another harness: that harness starts it fresh (the card's text and thread
|
|
677
699
|
// are its context), and the last session, if still open, is closed so two harnesses never work one card
|
|
@@ -680,7 +702,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
680
702
|
} else if (prev && prev.machine === machine) {
|
|
681
703
|
// a session the board closed, or one whose close its own command asked for (the next tick's sweep closes that
|
|
682
704
|
// run's pane), is not kept, whatever a fleet reading taken before the close still lists
|
|
683
|
-
|
|
705
|
+
const paneLive = prev.pane ? panePresent(prev.machine, prev.pane) : false;
|
|
706
|
+
if (paneLive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
|
|
707
|
+
if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
|
|
684
708
|
// the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
|
|
685
709
|
const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, ...kept } = prev;
|
|
686
710
|
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
@@ -759,12 +783,18 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
759
783
|
return session;
|
|
760
784
|
}
|
|
761
785
|
|
|
762
|
-
/**
|
|
786
|
+
/** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
|
|
787
|
+
function panePresent(machine, pane) {
|
|
788
|
+
const r = sc(['teams', 'panes', 'ls', '--machine', machine ?? localMachine()], { timeout: 60_000 });
|
|
789
|
+
const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
790
|
+
if (!Array.isArray(panes)) throw new Error(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
791
|
+
return panes.some((p) => p.pane === pane);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Whether a pane just launched is still there after the harness had a moment to start. */
|
|
763
795
|
function paneSurvives(machine, pane) {
|
|
764
796
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 6000);
|
|
765
|
-
|
|
766
|
-
const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
767
|
-
return Array.isArray(panes) ? panes.some((p) => p.pane === pane) : true; // no reading: the grace decides
|
|
797
|
+
return panePresent(machine, pane);
|
|
768
798
|
}
|
|
769
799
|
|
|
770
800
|
/** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.105",
|
|
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": {
|