@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.
Files changed (2) hide show
  1. package/board/dispatch.mjs +65 -20
  2. package/package.json +1 -1
@@ -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 on the card's machine (`supercode open --new … --on <machine>`), started with a
8
- // session id the dispatcher chose (`--session-id`) and recorded on the run. It stays open. It is live while
9
- // supercode's fleet lists it; one missing for the whole grace on an online machine is lost, and the next start
10
- // resumes it (`--resume`) before it ever replaces it from the card. Nothing asks the session about itself.
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.address ?? `sc:${session.machine}:claude-code:${session.session_id}`;
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 = session?.address ?? (session?.session_id && session.machine ? `sc:${session.machine}:claude-code:${session.session_id}` : null);
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.address ?? `sc:${session.machine}:claude-code:${session.session_id}`;
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 !== 'claude-code') return `a pane session's id is chosen at launch, which ${lane.harness} does not take; the lane's worker is claude-code`;
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 base = ['open', '--new', 'claude', '--detach', '--on', task.machine, '--cwd', task.workspace_path];
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.address ?? `sc:${prev.machine}:claude-code:${prev.session_id}`, input, `board-run-${task.id}-r${runId}`);
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
- const r = sc([...base, '--key', `board-${task.id}-r${runId}-resume`, '--input', told, '--', '--resume', prev.session_id, ...tail], { timeout: 180_000, as: boardAddress(board.label) });
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
- const sid = randomUUID();
626
- const r = sc([...base, '--key', `board-${task.id}-r${runId}-${sid.slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--', '--session-id', sid, ...tail], { timeout: 180_000, as: boardAddress(board.label) });
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
- session = { surface: 'pane', session_id: sid, machine, pane: lastJson(r.stdout)?.pane ?? null, mode: 'fresh', launched_at: now(), ...(replaced ? { replaces: replaced.session_id } : {}) };
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 = 'claude-code';
631
- session.address = `sc:${machine}:claude-code:${session.session_id}`;
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.address ?? `sc:${prev.machine}:claude-code:${prev.session_id}` : null;
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 !== 'claude-code') throw new Error(`${found.address} is ${harness}; the board resumes claude-code sessions by id`);
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.54",
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": {