@volter/supercode-orchestrator 0.4.54 → 0.4.56
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 +65 -20
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// review lane, failure breaker, caps, notify); what it adds is how a session runs, which the IR decides per
|
|
5
5
|
// profile (`worker.surface`):
|
|
6
6
|
//
|
|
7
|
-
// - `pane`: an interactive session
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// - `pane`: an interactive session of the lane's harness (Claude Code or Codex) on the card's machine (`supercode open
|
|
8
|
+
// --new … --on <machine>`), its session id recorded on the run: one the dispatcher chose (Claude Code's
|
|
9
|
+
// `--session-id`) or the one the harness named, read from the fleet by its pane (Codex). It stays open. It is live
|
|
10
|
+
// while supercode's fleet lists it; one missing for the whole grace on an online machine is lost, and the next start
|
|
11
|
+
// resumes it in its own harness. Nothing asks the session about itself.
|
|
11
12
|
// - `headless`: the profile's worker for one query on this machine (`bin/hermes-worker.mjs`, Hermes's worker
|
|
12
13
|
// argv and task environment), watched by its pid as Hermes watches its own.
|
|
13
14
|
//
|
|
@@ -82,6 +83,17 @@ export function observeFleet() {
|
|
|
82
83
|
return { ok: true, sessions, alive: new Set(sessions.map((s) => s.address).filter(Boolean)), online, error: rows ? null : 'teams machines list gave no answer; only this machine counts as online' };
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// how the launcher opens each harness a pane lane can run, and how it names the session: Claude Code is told the id
|
|
87
|
+
// it will have (`--session-id`) and resumed by it (`--resume`); Codex names its own session, which the board reads from
|
|
88
|
+
// the fleet by the pane it opened, and is resumed by it (`codex resume <id>`)
|
|
89
|
+
const HARNESSES = {
|
|
90
|
+
'claude-code': { program: 'claude', fresh: (sid) => ['--session-id', sid], resume: (sid) => ['--resume', sid], namesItself: false },
|
|
91
|
+
codex: { program: 'codex', fresh: () => [], resume: (sid) => ['resume', sid], namesItself: true },
|
|
92
|
+
};
|
|
93
|
+
/** A recorded session's fleet address (older records carry no harness: those were Claude Code's). */
|
|
94
|
+
const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
|
|
95
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
96
|
+
|
|
85
97
|
const sessionAlive = (fleet, session) => Boolean(session?.session_id) && [...fleet.alive].some((a) => a.endsWith(`:${session.session_id}`));
|
|
86
98
|
|
|
87
99
|
function pidAlive(pid) {
|
|
@@ -422,7 +434,7 @@ function closeRequested(board, report) {
|
|
|
422
434
|
const session = json(row.metadata, {})?.supercode;
|
|
423
435
|
if (!session?.pane) continue;
|
|
424
436
|
try { stopSession(board, session); } catch (error) { report.undelivered.push([row.task_id, session.address, `closing it: ${String(error.message).slice(-200)}`]); continue; }
|
|
425
|
-
const address = session
|
|
437
|
+
const address = addressOf(session);
|
|
426
438
|
tx(board, (db) => {
|
|
427
439
|
setRunSession(db, row.id, { ...session, closed_at: now() });
|
|
428
440
|
db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(row.task_id, address);
|
|
@@ -466,7 +478,7 @@ export async function sessionEffect(board, e) {
|
|
|
466
478
|
if (e.kind === 'send_to_actor') {
|
|
467
479
|
const run = tx(board, (db) => currentRun(db, e.id));
|
|
468
480
|
const session = run?.metadata?.supercode;
|
|
469
|
-
const address =
|
|
481
|
+
const address = addressOf(session);
|
|
470
482
|
if (!address) return;
|
|
471
483
|
await send(boardAddress(board.label), board.label, address, e.text, `board-actor-${e.id}-${now()}`);
|
|
472
484
|
return;
|
|
@@ -483,7 +495,7 @@ export async function sessionEffect(board, e) {
|
|
|
483
495
|
if (caller && caller === session.session_id) { tx(board, (db) => setRunSession(db, row.id, { ...session, close_requested: now() })); return; }
|
|
484
496
|
stopSession(board, session);
|
|
485
497
|
// a closed session hears nothing more of the card
|
|
486
|
-
const address = session
|
|
498
|
+
const address = addressOf(session);
|
|
487
499
|
tx(board, (db) => {
|
|
488
500
|
setRunSession(db, row.id, { ...session, closed_at: now() });
|
|
489
501
|
db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(e.id, address);
|
|
@@ -521,7 +533,7 @@ function launcherCheck(db, task, lane, fleet) {
|
|
|
521
533
|
// a card on this machine gets its workspace made here (scratch, dir or worktree, as Hermes makes it); another
|
|
522
534
|
// machine's card names its directory there
|
|
523
535
|
if (machine !== localMachine() && (task.workspace_kind !== 'dir' || !task.workspace_path)) return `a pane session on ${machine} starts in a directory there: the card's workspace is dir:<absolute path>, not ${task.workspace_kind}`;
|
|
524
|
-
if (lane.harness
|
|
536
|
+
if (!HARNESSES[lane.harness]) return `the board opens pane sessions of ${Object.keys(HARNESSES).join(' and ')}; the lane's worker is ${lane.harness}`;
|
|
525
537
|
return null;
|
|
526
538
|
}
|
|
527
539
|
|
|
@@ -578,7 +590,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
578
590
|
}
|
|
579
591
|
const model = task.model_override ?? lane.model;
|
|
580
592
|
const tail = model ? ['--model', model] : [];
|
|
581
|
-
const
|
|
593
|
+
const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
|
|
594
|
+
const how = HARNESSES[harness];
|
|
595
|
+
const base = ['open', '--new', how.program, '--detach', '--on', task.machine, '--cwd', task.workspace_path];
|
|
582
596
|
// the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
|
|
583
597
|
// a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
|
|
584
598
|
// session starts (never two processes on one session)
|
|
@@ -601,16 +615,18 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
601
615
|
const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, close_requested: _q, closed_at: _c, ...kept } = prev;
|
|
602
616
|
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
603
617
|
// a kept session is told what this run is for (its card came back to it, with what happened since)
|
|
604
|
-
if (input) deliverNow(board, prev
|
|
618
|
+
if (input) deliverNow(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`);
|
|
605
619
|
} else if (prev.resumable !== false) {
|
|
606
620
|
// a resumed session is told what this run is for, as a kept one is: the run's own prompt (a reviewer resumed for
|
|
607
621
|
// its next round gets that round's prompt, its arc's context with it), not only that it was resumed
|
|
608
622
|
const told = input ? `The board resumed this session for this run of card ${task.id}.\n\n${input}` : `The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`;
|
|
609
|
-
|
|
623
|
+
// a session is resumed in its own harness, whatever the lane's is now
|
|
624
|
+
const its = HARNESSES[prev.harness ?? 'claude-code'] ?? HARNESSES['claude-code'];
|
|
625
|
+
const r = sc(['open', '--new', its.program, ...base.slice(3), '--key', `board-${task.id}-r${runId}-resume`, '--input', told, '--', ...its.resume(prev.session_id), ...tail], { timeout: 180_000, as: boardAddress(board.label) });
|
|
610
626
|
const pane = r.code === 0 ? lastJson(r.stdout)?.pane ?? null : null;
|
|
611
627
|
// a resume the harness refuses (its transcript is gone) ends the pane at once; another machine's launch
|
|
612
628
|
// answers before that shows, so the pane is looked for again a moment later
|
|
613
|
-
if (pane && paneSurvives(task.machine, pane)) session = { surface: 'pane', session_id: prev.session_id, machine, pane, mode: 'resume', launched_at: now() };
|
|
629
|
+
if (pane && paneSurvives(task.machine, pane)) session = { surface: 'pane', session_id: prev.session_id, harness: prev.harness ?? 'claude-code', machine, pane, mode: 'resume', launched_at: now() };
|
|
614
630
|
else replaced = { session_id: prev.session_id, why: r.code === 0 ? `its resume ended as soon as it started (pane ${pane})` : (r.stderr || r.stdout).trim().slice(-300) };
|
|
615
631
|
} else replaced = { session_id: prev.session_id, why: 'its resume never came up' };
|
|
616
632
|
} else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
|
|
@@ -622,13 +638,20 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
622
638
|
throw failed;
|
|
623
639
|
}
|
|
624
640
|
if (!session) {
|
|
625
|
-
|
|
626
|
-
const r = sc([...base, '--key', `board-${task.id}-r${runId}-${sid.slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--',
|
|
641
|
+
let sid = how.namesItself ? null : randomUUID();
|
|
642
|
+
const r = sc([...base, '--key', `board-${task.id}-r${runId}-${(sid ?? randomUUID()).slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--', ...(sid ? how.fresh(sid) : how.fresh()), ...tail], { timeout: 180_000, as: boardAddress(board.label) });
|
|
627
643
|
if (r.code !== 0) throw new Error(`supercode open --on ${task.machine}: ${(r.stderr || r.stdout).trim().slice(-500)}`);
|
|
628
|
-
|
|
644
|
+
const pane = lastJson(r.stdout)?.pane ?? null;
|
|
645
|
+
// a harness that names its own session is found in the fleet by the pane the board opened for it
|
|
646
|
+
for (let waited = 0; !sid && pane && waited < 90_000; waited += 3000) {
|
|
647
|
+
pause(3000);
|
|
648
|
+
sid = observeFleet().sessions?.find((f) => f.pane === pane && String(f.address).split(':')[2] === harness)?.address.split(':')[3] ?? null;
|
|
649
|
+
}
|
|
650
|
+
if (!sid) throw new Error(`${harness} in pane ${pane ?? '(none)'} on ${task.machine} named no session within 90 s`);
|
|
651
|
+
session = { surface: 'pane', session_id: sid, machine, pane, mode: 'fresh', launched_at: now(), ...(replaced ? { replaces: replaced.session_id } : {}) };
|
|
629
652
|
}
|
|
630
|
-
session.harness =
|
|
631
|
-
session.address = `sc:${machine}
|
|
653
|
+
session.harness = session.harness ?? harness;
|
|
654
|
+
session.address = `sc:${machine}:${session.harness}:${session.session_id}`;
|
|
632
655
|
Object.assign(session, meta);
|
|
633
656
|
const kind = { adopt: 'session_adopted', resume: 'session_resumed', fresh: replaced ? 'session_replaced' : 'session_started' }[session.mode];
|
|
634
657
|
tx(board, (db) => {
|
|
@@ -636,7 +659,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
636
659
|
event(db, task.id, kind, { session_id: session.session_id, machine, pane: session.pane, address: session.address, mode: session.mode, ...(replaced ? { replaced } : {}) }, runId);
|
|
637
660
|
// the working session hears updates to its card; the slot's previous session no longer does (a reviewer starting
|
|
638
661
|
// leaves the implementer's, and a subtask's arc session's, subscription alone)
|
|
639
|
-
const before = prev ? prev
|
|
662
|
+
const before = prev ? addressOf(prev) : null;
|
|
640
663
|
if (before && before !== session.address) db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(task.id, before);
|
|
641
664
|
subscribeIn(db, task.id, session.address, 'worker');
|
|
642
665
|
// it sees the cards its own depends on change, and works its subtasks
|
|
@@ -737,7 +760,7 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
737
760
|
const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
|
|
738
761
|
if (!found) throw new Error(`the fleet lists no live session ${who}`);
|
|
739
762
|
const [, host, harness, sessionId] = found.address.split(':');
|
|
740
|
-
if (harness
|
|
763
|
+
if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
|
|
741
764
|
// the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
|
|
742
765
|
let dir = workspace;
|
|
743
766
|
if (!dir) {
|
|
@@ -814,6 +837,27 @@ export function noticeOf(entry, audience, kind, task, payload, comment = null) {
|
|
|
814
837
|
return text.trim() ? text : null;
|
|
815
838
|
}
|
|
816
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Who hears a card without having asked: each running card's session hears its card and its subtasks (a subscription a
|
|
842
|
+
* launch or a missed fleet read lost comes back), and each of the workflow's managers (`params.managers`) hears every
|
|
843
|
+
* open card as the `manager` audience, whoever filed it, when the workflow's notify names one.
|
|
844
|
+
*/
|
|
845
|
+
function ensureSubscriptions(board, entry) {
|
|
846
|
+
const managers = Array.isArray(entry.params.managers) ? entry.params.managers.map(String) : [];
|
|
847
|
+
const toManagers = Object.keys(entry.workflow.notify?.manager ?? {}).length > 0;
|
|
848
|
+
tx(board, (db) => {
|
|
849
|
+
for (const t of db.prepare("SELECT id, current_run_id FROM tasks WHERE status NOT IN ('done', 'archived')").all()) {
|
|
850
|
+
if (toManagers) for (const m of managers) subscribeIn(db, t.id, m, 'manager');
|
|
851
|
+
if (!t.current_run_id) continue;
|
|
852
|
+
const session = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(t.current_run_id)?.metadata, {})?.supercode;
|
|
853
|
+
const address = addressOf(session);
|
|
854
|
+
if (!address || (session.slot ?? 'worker') !== 'worker') continue;
|
|
855
|
+
subscribeIn(db, t.id, address, 'worker');
|
|
856
|
+
for (const st of db.prepare('SELECT task_id FROM supercode_cards WHERE subtask_of = ?').all(t.id)) subscribeIn(db, st.task_id, address, 'worker');
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
817
861
|
/** The event kinds an audience hears (the workflow's notify). */
|
|
818
862
|
const heardBy = (entry, audience) => Object.keys(entry.workflow.notify?.[audience] ?? {});
|
|
819
863
|
|
|
@@ -821,7 +865,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
821
865
|
const from = boardAddress(board.label);
|
|
822
866
|
// each subscription's notices go in order; different subscribers are told side by side
|
|
823
867
|
await pool(subscriptions(board.db).filter((sub) => sub.platform === 'supercode'), SEND_CONCURRENCY, async (sub) => {
|
|
824
|
-
const role = ['worker', 'dependency'].includes(sub.delivery_metadata?.role) ? sub.delivery_metadata.role : 'creator';
|
|
868
|
+
const role = ['worker', 'dependency', 'manager'].includes(sub.delivery_metadata?.role) ? sub.delivery_metadata.role : 'creator';
|
|
825
869
|
// a session the fleet does not list this tick is told next tick: one reading is not a loss (a loaded box drops
|
|
826
870
|
// sessions from a read, and a dropped subscription never came back), and a session that is replaced on its card
|
|
827
871
|
// has its subscription moved when the new one starts
|
|
@@ -1141,6 +1185,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
1141
1185
|
// what the moves asked for, once they are committed (messages to actors, closed panes, cleaned workspaces)
|
|
1142
1186
|
await runEffects(board, effects);
|
|
1143
1187
|
phase('goals');
|
|
1188
|
+
ensureSubscriptions(board, entry);
|
|
1144
1189
|
await notify(board, fleet, report, entry); phase('notify');
|
|
1145
1190
|
fireHooks(board, settings, report);
|
|
1146
1191
|
if (String(board.db.prepare('PRAGMA journal_mode').get()?.journal_mode).toLowerCase() === 'wal') board.db.prepare('PRAGMA wal_checkpoint(PASSIVE)').get();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.56",
|
|
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": {
|