@volter/supercode-orchestrator 0.4.126 → 0.4.127

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/cli.mjs CHANGED
@@ -54,6 +54,8 @@ supercode's own:
54
54
  notify-subscribe <id> --address sc:<machine>:<harness>:<id> [--role creator|worker|dependency]
55
55
  Any verb … --at sc:<machine>:board:<home>: sent as mail to a board kept on another machine.
56
56
  <event> <id>... [reason] [--<field> value]: any other event the board's workflow declares, its flags the event's fields.
57
+ start <id> release a --no-start hold and start through the declared workflow
58
+ create ... --adopt <session> hold dispatch until that live session is attached
57
59
  goto <id> <status> [reason] [--<field> value]: the event the workflow says takes the card to that status, sent.
58
60
 
59
61
  specify/decompose with no spec start a session on the triage card to write it. A card's own session may comment on,
@@ -294,11 +296,15 @@ export async function runWorkflow(argv) {
294
296
  workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
295
297
  triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
296
298
  maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? callerName(),
299
+ noStart: Boolean(args['no-start']), adopt: args.adopt ?? null,
297
300
  every: args.every ?? null, sessionId: callerSession(), subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
298
301
  project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : args['no-goal'] ? false : null,
299
302
  }), true);
300
303
  let answer = { id: created.id, existing: created.existing };
301
- if (!args['no-start'] && !created.existing && !args['subtask-of']) {
304
+ if (args.adopt) {
305
+ const adopted = withBoard(b => adoptSession(b, created.id, args.adopt, { ttl: settings.ttl }));
306
+ answer = { ...answer, started: true, session: adopted.address, mode: adopted.mode };
307
+ } else if (!args['no-start'] && !created.existing && !args['subtask-of']) {
302
308
  const [report] = await tick({ root, lanes, settings, only: created.id, slugs: [slug] });
303
309
  const started = report.started.find(([t]) => t === created.id);
304
310
  const queued = report.queued.find(([t]) => t === created.id);
@@ -813,6 +819,12 @@ export async function runWorkflow(argv) {
813
819
  say(args, lifted.length ? `Resumed (removed ${lifted.join(', ')})` : 'Not paused', { ok: true, lifted });
814
820
  return 0;
815
821
  }
822
+ case 'start': {
823
+ need(1, 'a card id');
824
+ const { lanes, settings } = await laneState(root);
825
+ const session = await withBoardAsync(b => startCard(b, id, { lanes, settings, roles: rolesFor(b.db, id, sid, workflowFor(root).params) }));
826
+ say(args, `${id}: ${session?.address ?? 'started'}`, { ok: true, session }); return 0;
827
+ }
816
828
  case 'adopt': {
817
829
  need(2, 'a card id and a session');
818
830
  const { settings } = await laneState(root);
@@ -136,7 +136,20 @@ const HARNESSES = {
136
136
  const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
137
137
  const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
138
138
 
139
- const sessionAlive = (fleet, session) => Boolean(session?.session_id) && [...fleet.alive].some((a) => a.endsWith(`:${session.session_id}`));
139
+ const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : Boolean(session?.session_id) && fleet.alive.has(addressOf(session));
140
+
141
+ /** Native activity comes from the machine's harness discovery, not catalogue age. */
142
+ function nativeSessions(machine, harness) {
143
+ const result = sc(['discover', '--on', machine, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
144
+ const page = result.code === 0 ? lastJson(result.stdout) : null;
145
+ if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
146
+ return page.sessions;
147
+ }
148
+ function nativeReading(rows, session) {
149
+ const row = rows.find(r => r.locator?.session_id === session.session_id && r.locator?.harness === session.harness);
150
+ const presence = row?.activity?.presence;
151
+ return { row, alive: presence === 'running' ? true : presence === 'persisted' ? false : null };
152
+ }
140
153
 
141
154
  /** Match a manual continuation by the same harness identity and an actually live machine pane. */
142
155
  function continuationPane(fleet, session, panes) {
@@ -211,6 +224,17 @@ function supervise(board, fleet, settings, report, entry, effects) {
211
224
  // The catalog can lag a pane. Read the actual machines once, before any board mutation.
212
225
  const observed = new Map();
213
226
  const exits = new Map();
227
+ const native = new Map(); fleet.native = new Map();
228
+ for (const { id } of rows) {
229
+ const session = currentRun(board.db, id)?.metadata?.supercode;
230
+ if (!session?.session_id || session.pane || session.surface === 'headless') continue;
231
+ const key = `${session.machine}:${session.harness}`;
232
+ if (!native.has(key)) {
233
+ try { native.set(key, nativeSessions(session.machine, session.harness)); }
234
+ catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
235
+ }
236
+ fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
237
+ }
214
238
  for (const { id } of rows) {
215
239
  const s = currentRun(board.db, id)?.metadata?.supercode;
216
240
  if (!s?.pane || observed.has(s.machine)) continue;
@@ -288,7 +312,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
288
312
  // Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
289
313
  const paneReading = session?.pane ? observed.get(session.machine) : undefined;
290
314
  const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
291
- const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) : true) : false;
315
+ const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
292
316
  // (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
293
317
  if ((!task.claim_lock || !task.claim_expires) && !live) {
294
318
  observe('orphaned', { reason: 'orphaned_running', claim_lock: task.claim_lock, claim_expires: task.claim_expires, error: 'orphaned running card (broken claim bookkeeping)' }, 'reclaimed');
@@ -325,6 +349,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
325
349
  if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
326
350
  if (!session.pane && !fleet.ok) return;
327
351
  const alive = session.pane ? paneLive : sessionAlive(fleet, session);
352
+ if (alive == null) return; // native discovery was inconclusive
328
353
  const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
329
354
  const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
330
355
  const missing = !alive && machineUp && !young;
@@ -374,6 +399,13 @@ function stopSession(board, session) {
374
399
  if (pidAlive(session.pid)) signal('SIGKILL');
375
400
  return;
376
401
  }
402
+ if (!session.pane && session.surface === 'native') {
403
+ const r = sc(['close', addressOf(session)], { timeout: 120_000, as: boardAddress(board.label) });
404
+ if (r.code !== 0) throw new Error(`closing native session ${addressOf(session)}: ${(r.stderr || r.stdout).trim().slice(-500)}`);
405
+ const reading = nativeReading(nativeSessions(session.machine, session.harness), session);
406
+ if (reading.alive !== false) throw new Error(`native close has no confirmed stopped receipt for ${addressOf(session)}`);
407
+ return;
408
+ }
377
409
  if (session.pane) {
378
410
  const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
379
411
  if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
@@ -421,6 +453,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
421
453
  for (const task of everyCard) {
422
454
  if (only && task.id !== only) continue;
423
455
  const state = scopeFor(task).card;
456
+ if (state.dispatch_hold) continue;
424
457
  if (workflow.run && (task.subtask_of || state.paused || state.archived || state.blocked_by.length || task.current_run_id || (state.every && state.next_run_at > now()))) continue;
425
458
  if (!startable.has(statusOf(board.db, task))) continue;
426
459
  const plan = planOf(task, scopeFor(task));
@@ -565,6 +598,8 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
565
598
  const moved = tx(board, (db) => {
566
599
  assertWorkspaceOwner(db, before, checked);
567
600
  if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
601
+ if (!roles.includes('manager')) throw new Error('Only the manager starts a held card; request its decision.');
602
+ event(db, id, 'dispatch_released', { reason: 'explicit start' });
568
603
  return fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } });
569
604
  });
570
605
  const task = getTaskNow(board, id);
@@ -611,7 +646,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
611
646
  report.undelivered.push([row.task_id, addressOf(session), error]);
612
647
  }
613
648
  }
614
- if (!session?.pane || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
649
+ if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
615
650
  if (caller && session.session_id === caller) continue; // let the command that ended its own run return
616
651
  const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
617
652
  // Persist retries even for historical ends that did not have a close_session action.
@@ -619,6 +654,16 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
619
654
  let error = null;
620
655
  try {
621
656
  const machine = session.machine ?? localMachine();
657
+ if (session.surface === 'native' && !session.pane) {
658
+ const reading = nativeReading(nativeSessions(machine, session.harness), session);
659
+ if (reading.alive == null) throw new Error(`close deferred: native liveness unknown for ${addressOf(session)}`);
660
+ if (reading.alive) {
661
+ const protectedNow = active.some(r => addressOf(json(r.metadata, {})?.supercode) === addressOf(session));
662
+ if (protectedNow) throw new Error('close deferred: native session is owned by an unended run');
663
+ if (session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${session.lifecycle_notice_pending} has not been delivered`);
664
+ stopSession(board, session);
665
+ }
666
+ } else {
622
667
  if (!panes.has(machine)) {
623
668
  const r = sc(['teams', 'panes', 'ls', ...onMachine(machine)], { timeout: 60_000, as: boardAddress(board.label) });
624
669
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
@@ -634,10 +679,11 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
634
679
  if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
635
680
  stopSession(board, session); observed.live.delete(session.pane);
636
681
  }
682
+ }
637
683
  } catch (e) { error = String(e.message).slice(-500); }
638
684
  tx(board, (db) => {
639
685
  const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
640
- if (!current || current.pane !== session.pane) return;
686
+ if (!current || current.pane !== session.pane || current.session_id !== session.session_id) return;
641
687
  const at = now();
642
688
  if (error) {
643
689
  setRunSession(db, row.id, { ...current, close_attempted_at: at, close_attempts: Number(current.close_attempts ?? 0) + 1, close_error: error });
@@ -668,8 +714,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
668
714
  if (!(entry.workflow.statuses[status]?.on?.session_idle || entry.workflow.on?.session_idle || entry.workflow.run?.on?.session_idle)) return;
669
715
  const runRow = currentRun(db, id);
670
716
  const session = runRow?.metadata?.supercode;
671
- if (!session?.session_id || session.surface !== 'pane' || session.offline_since != null) return;
672
- const live = fleet.sessions.find((x) => x.address?.endsWith(`:${session.session_id}`));
717
+ if (!session?.session_id || !['pane', 'native'].includes(session.surface) || session.offline_since != null) return;
718
+ if (session.surface === 'native' && fleet.native?.get(addressOf(session))?.alive !== true) return;
719
+ const live = fleet.sessions.find((x) => x.address === addressOf(session));
673
720
  const observedAt = now();
674
721
  const lastAt = Math.floor(Number(live?.last_message_at_ms) / 1000);
675
722
  const reading = {
@@ -695,7 +742,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
695
742
  try {
696
743
  const where = whereOf(board, task);
697
744
  // the ask names what the session may do, as its run's prompt did
698
- const said = doors(entry.workflow, status, session.role ?? runSpec(entry, status)?.role ?? 'implementer', id, where, session.surface === 'pane' ? 'pane' : 'headless');
745
+ const said = doors(entry.workflow, status, session.role ?? runSpec(entry, status)?.role ?? 'implementer', id, where, session.surface !== 'headless' ? 'pane' : 'headless');
699
746
  const r = fire(db, board, entry, id, 'session_idle', { roles: ['platform'], payload: { ...reading, turns: reading.turns_before, worked }, extra: { where, doors: said } });
700
747
  effects.push(...r.effects);
701
748
  const after = currentRun(db, id);
@@ -948,7 +995,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
948
995
  closeRequested(board, { undelivered: [] }, { taskId: task.id });
949
996
  const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
950
997
  .map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
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);
998
+ .find((r) => (r.session?.pane || r.session?.launch_key || r.session?.surface === 'native') && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
952
999
  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'}`);
953
1000
  let prev = view(board, (db) => lastSession(db, task.id, slot));
954
1001
  if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
@@ -997,6 +1044,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
997
1044
  };
998
1045
  let session = null;
999
1046
  let replaced = null;
1047
+ if (prev?.surface === 'native' && !prev.pane) {
1048
+ const reading = nativeReading(nativeSessions(prev.machine, prev.harness), prev);
1049
+ if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1050
+ fleet.native ??= new Map(); fleet.native.set(addressOf(prev), reading);
1051
+ if (reading.alive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw unavailable(`native session ${addressOf(prev)} is still live; its close must finish before replacement`);
1052
+ }
1000
1053
  if (prev?.forgotten) {
1001
1054
  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`);
1002
1055
  replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
@@ -1165,14 +1218,15 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
1165
1218
  if (!found) throw new Error(`the fleet lists no live session ${who}`);
1166
1219
  const [, host, harness, sessionId] = found.address.split(':');
1167
1220
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
1168
- if (!found.pane || !panePresent(host, found.pane)) throw new Error(`${found.address} has no verified live pane`);
1221
+ const native = found.pane ? null : nativeReading(nativeSessions(host, harness), { session_id: sessionId, harness });
1222
+ if (found.pane ? !panePresent(host, found.pane) : native.alive !== true) throw new Error(`${found.address} has no verified live native process or pane`);
1169
1223
  const beforeRun = view(board, (db) => currentRun(db, id));
1170
1224
  const beforeSession = beforeRun?.metadata?.supercode;
1171
1225
  if (beforeRun && beforeSession?.session_id !== sessionId) throw new Error(`${id} has another active run/session; end it before adopting a different identity`);
1172
1226
  if (beforeSession?.pane && beforeSession.pane !== found.pane && panePresent(beforeSession.machine, beforeSession.pane)) throw new Error(`${id} still has live pane ${beforeSession.pane}; close the extra pane before rebinding ${sessionId}`);
1173
1227
 
1174
1228
  // the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
1175
- let dir = workspace;
1229
+ let dir = workspace ?? native?.row?.cwd;
1176
1230
  if (!dir) {
1177
1231
  const r = sc(['discover', '--on', host, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
1178
1232
  dir = (json(r.stdout.trim(), {})?.sessions ?? []).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
@@ -1203,10 +1257,11 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
1203
1257
  const implementer = lastSession(db, id, 'worker');
1204
1258
  if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
1205
1259
  }
1260
+ event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
1206
1261
  if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: ['manager'], payload: { lock: claimer() } });
1207
1262
  const runId = getTask(db, id).current_run_id;
1208
1263
  if (!runId) throw new Error(`${id} waits on parents that are not done`);
1209
- const session = { surface: 'pane', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now() };
1264
+ const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now() };
1210
1265
  setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: currentRun(db, id)?.metadata?.supercode?.slot ?? beforeSession?.slot ?? expected?.session ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
1211
1266
  event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt', role: currentRun(db, id)?.metadata?.supercode?.role }, runId);
1212
1267
  subscribeIn(db, id, found.address, 'worker');
package/board/engine.mjs CHANGED
@@ -216,6 +216,7 @@ export function createCard(board, spec) {
216
216
  const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
217
217
  every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
218
218
  event(db, id, 'created', { creator: actorOf(), assignee: spec.assignee || null, status, parents, tenant: spec.tenant ?? null, ...(machine ? { machine } : {}), ...(arc ? { subtask_of: arc.id } : {}) });
219
+ if ((spec.noStart || spec.adopt) && !arc) event(db, id, 'dispatch_held', { session: spec.adopt ?? null, reason: spec.adopt ? 'awaiting adoption' : 'created with --no-start' });
219
220
  if (parents.length) inheritSubscriptions(db, id, parents);
220
221
  if (arc) {
221
222
  // the arc's subscribers hear its subtasks, and the arc's session works them
@@ -222,6 +222,7 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
222
222
  const card = {
223
223
  ...task,
224
224
  creator_address: creatorOf(db, task.id),
225
+ dispatch_hold: one(db, "SELECT kind FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','session_adopted') ORDER BY id DESC LIMIT 1", task.id)?.kind === 'dispatch_held',
225
226
  status,
226
227
  blocked_by: blocks,
227
228
  paused: json(state.paused, null),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.126",
3
+ "version": "0.4.127",
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": {