@volter/supercode-orchestrator 0.4.106 → 0.4.107

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.
@@ -63,6 +63,8 @@ function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
63
63
  }
64
64
 
65
65
  const lastJson = (text) => {
66
+ const whole = json(text.trim(), null);
67
+ if (whole && typeof whole === 'object') return whole;
66
68
  const line = text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('{') || l.startsWith('[')).pop();
67
69
  return line ? json(line, null) : null;
68
70
  };
@@ -120,6 +122,16 @@ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0,
120
122
 
121
123
  const sessionAlive = (fleet, session) => Boolean(session?.session_id) && [...fleet.alive].some((a) => a.endsWith(`:${session.session_id}`));
122
124
 
125
+ /** Match a manual continuation by the same harness identity and an actually live machine pane. */
126
+ function continuationPane(fleet, session, panes) {
127
+ if (!session?.session_id || !(panes instanceof Set)) return null;
128
+ const candidates = new Set((fleet.sessions ?? []).filter((s) => s.address === addressOf(session) && s.pane && panes.has(s.pane)).map((s) => s.pane));
129
+ if (session.pane && panes.has(session.pane)) candidates.add(session.pane);
130
+ if (candidates.size > 1) throw new Error(`session ${session.session_id} has multiple live panes: ${[...candidates].join(', ')}; close the extra pane before continuing`);
131
+ return [...candidates][0] ?? null;
132
+ }
133
+
134
+
123
135
  function pidAlive(pid) {
124
136
  if (!pid) return false;
125
137
  try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
@@ -191,7 +203,17 @@ function supervise(board, fleet, settings, report, entry, effects) {
191
203
  const task = getTask(db, id);
192
204
  if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
193
205
  const runRow = currentRun(db, id);
194
- const session = runRow?.metadata?.supercode ?? null;
206
+ let session = runRow?.metadata?.supercode ?? null;
207
+ if (session?.pane) {
208
+ let found;
209
+ try { found = continuationPane(fleet, session, observed.get(session.machine)); }
210
+ catch (error) { report.failed.push([id, error.message]); return; }
211
+ if (found && found !== session.pane) {
212
+ session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
213
+ setRunSession(db, runRow.id, session);
214
+ event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
215
+ }
216
+ }
195
217
  const t = now();
196
218
  // what the dispatcher saw becomes a platform event; the card's workflow says what it does
197
219
  const observe = (name, payload, bucket) => {
@@ -485,10 +507,6 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
485
507
  if (!session?.pane || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
486
508
  if (caller && session.session_id === caller) continue; // let the command that ended its own run return
487
509
  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());
488
- if (active.some((r) => {
489
- const s = json(r.metadata, {})?.supercode;
490
- return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
491
- })) continue;
492
510
  // Persist retries even for historical ends that did not have a close_session action.
493
511
  tx(board, (db) => requestSessionClose(db, row.task_id, { runId: row.id }));
494
512
  let error = null;
@@ -676,6 +694,16 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
676
694
  .find((r) => r.session?.pane && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
677
695
  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'}`);
678
696
  let prev = view(board, (db) => lastSession(db, task.id, slot));
697
+ if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
698
+ const fresh = observeFleet();
699
+ if (!fresh.ok) throw new Error(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
700
+ const r = sc(['teams', 'panes', 'ls', '--machine', machine], { timeout: 60_000 });
701
+ const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
702
+ if (!Array.isArray(listed)) throw new Error(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
703
+ const pane = continuationPane(fresh, prev, new Set(listed.map((p) => p.pane)));
704
+ if (pane && pane !== prev.pane) prev = { ...prev, pane, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
705
+ fleet = fresh;
706
+ }
679
707
  if (prev?.close_requested && !prev.closed_at) {
680
708
  const report = { undelivered: [] };
681
709
  closeRequested(board, report, { taskId: task.id });
@@ -886,6 +914,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
886
914
  if (!found) throw new Error(`the fleet lists no live session ${who}`);
887
915
  const [, host, harness, sessionId] = found.address.split(':');
888
916
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
917
+ if (!found.pane || !panePresent(host, found.pane)) throw new Error(`${found.address} has no verified live pane`);
918
+ const beforeRun = view(board, (db) => currentRun(db, id));
919
+ const beforeSession = beforeRun?.metadata?.supercode;
920
+ if (beforeRun && beforeSession?.session_id !== sessionId) throw new Error(`${id} has another active run/session; end it before adopting a different identity`);
921
+ 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}`);
922
+
889
923
  // the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
890
924
  let dir = workspace;
891
925
  if (!dir) {
@@ -899,7 +933,8 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
899
933
  return tx(board, (db) => {
900
934
  const task = getTask(db, id);
901
935
  if (!task) throw new Error(`no card ${id}`);
902
- if (task.status === 'running') throw new Error(`${id} is running already`);
936
+ if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
937
+ if (beforeRun && assignee && assignee !== task.assignee) throw new Error('rebinding a session does not reassign its active run');
903
938
  if (task.machine && normalMachine(task.machine) !== host) throw new Error(`${id} runs on ${task.machine}, and ${found.address} is on ${host}; move the card first`);
904
939
  const was = lastSession(db, id);
905
940
  if (was && was.session_id !== sessionId && sessionAlive(fleet, was)) throw new Error(`${id} already has a live session, ${was.address}`);
@@ -910,11 +945,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
910
945
  db.prepare("UPDATE tasks SET workspace_kind = 'dir', workspace_path = ? WHERE id = ?").run(dir.replace(/^dir:/, ''), id);
911
946
  }
912
947
  // the workflow's adopt event: the card is claimed for its implementer, whose session this now is
913
- fire(db, board, workflowFor(board.root), id, 'adopt', { roles: ['manager'], payload: { lock: claimer() } });
948
+ const entry = workflowFor(board.root);
949
+ if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: ['manager'], payload: { lock: claimer() } });
914
950
  const runId = getTask(db, id).current_run_id;
915
951
  if (!runId) throw new Error(`${id} waits on parents that are not done`);
916
952
  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() };
917
- setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: 'worker' });
953
+ setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: beforeSession?.slot ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
918
954
  event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt' }, runId);
919
955
  subscribeIn(db, id, found.address, 'worker');
920
956
  for (const p of db.prepare('SELECT parent_id FROM task_links WHERE child_id = ?').all(id)) subscribeIn(db, p.parent_id, found.address, 'dependency');
package/board/ztrack.mjs CHANGED
@@ -528,12 +528,18 @@ class ZtrackBoard {
528
528
  if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_export_events'").get()) {
529
529
  compat.exec(`CREATE TABLE supercode_export_events (source_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL UNIQUE)`);
530
530
  compat.exec(`INSERT INTO supercode_export_events SELECT id, id FROM task_events WHERE json_extract(payload, '$.__supercode_export') = 1`);
531
+ // Repair existing lowered holds on upgrade, once; subsequent exports stay proportional to changes.
532
+ for (const state of this.mem.prepare('SELECT task_id, paused, blocked_by FROM supercode_cards').all()) {
533
+ if (!json(state.paused, null) && !json(state.blocked_by, []).some((b) => b.kind === 'message')) continue;
534
+ projected.add(state.task_id); projectStatus(this.mem, this.board(), entry, state.task_id);
535
+ }
531
536
  }
532
537
  // Every import is event-based. Existing unconsumed acts must never be erased by an export race.
533
538
  const cursor = this.get('hermes_cursor', 0);
534
539
  const unseen = compat.prepare('SELECT payload FROM task_events WHERE id > ?').all(cursor).some((r) => !json(r.payload, {})?.__supercode_export);
535
540
  if (unseen) throw new Error('Hermes wrote during board export; retained its events for the next read');
536
541
  const sources = new Map(changes.map(c => [`${c.source.name}:${c.source.key}`, c.source]));
542
+ for (const id of projected) sources.set(`tasks:${JSON.stringify([id])}`, { name: 'tasks', key: JSON.stringify([id]), row: this.mem.prepare('SELECT * FROM tasks WHERE id = ?').get(id) });
537
543
  for (const source of [...sources.values()]) {
538
544
  const id = source.name === 'tasks' ? source.row?.id : source.name === 'supercode_cards' ? source.row?.task_id : null;
539
545
  if (id) sources.set(`tasks:${JSON.stringify([id])}`, {name:'tasks',key:JSON.stringify([id]),row:this.mem.prepare('SELECT * FROM tasks WHERE id=?').get(id)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.106",
3
+ "version": "0.4.107",
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": {