@volter/supercode-orchestrator 0.4.103 → 0.4.104

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 +24 -7
  2. package/package.json +1 -1
@@ -500,7 +500,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
500
500
  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
501
  } else {
502
502
  const { close_error: _error, ...rest } = current;
503
- setRunSession(db, row.id, { ...rest, closed_at: at, close_confirmed_at: at });
503
+ setRunSession(db, row.id, { ...rest, close_error: null, closed_at: at, close_confirmed_at: at });
504
504
  const address = addressOf(session);
505
505
  const subscribed = active.some((r) => addressOf(json(r.metadata, {})?.supercode) === address && r.id !== row.id);
506
506
  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 +651,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
651
651
  // the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
652
652
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
653
653
  // session starts (never two processes on one session)
654
+ // Every old pane in the slot must be settled, including a failed launch with no session id.
655
+ closeRequested(board, { undelivered: [] }, { taskId: task.id });
656
+ const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
657
+ .map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
658
+ .find((r) => r.session?.pane && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
659
+ 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
660
  let prev = view(board, (db) => lastSession(db, task.id, slot));
655
661
  if (prev?.close_requested && !prev.closed_at) {
656
662
  const report = { undelivered: [] };
@@ -671,7 +677,10 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
671
677
  };
672
678
  let session = null;
673
679
  let replaced = null;
674
- if (prev?.forgotten) replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
680
+ if (prev?.forgotten) {
681
+ 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`);
682
+ replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
683
+ }
675
684
  else if (prev && (prev.harness ?? 'claude-code') !== harness) {
676
685
  // the card was reassigned to a lane of another harness: that harness starts it fresh (the card's text and thread
677
686
  // are its context), and the last session, if still open, is closed so two harnesses never work one card
@@ -680,7 +689,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
680
689
  } else if (prev && prev.machine === machine) {
681
690
  // a session the board closed, or one whose close its own command asked for (the next tick's sweep closes that
682
691
  // run's pane), is not kept, whatever a fleet reading taken before the close still lists
683
- if (!prev.closed_at && !prev.close_requested && sessionAlive(fleet, prev)) {
692
+ const paneLive = prev.pane ? panePresent(prev.machine, prev.pane) : false;
693
+ 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`);
694
+ if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
684
695
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
685
696
  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
697
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
@@ -759,12 +770,18 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
759
770
  return session;
760
771
  }
761
772
 
762
- /** Whether a pane just launched on `machine` is still there after the harness had a moment to start. */
773
+ /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
774
+ function panePresent(machine, pane) {
775
+ const r = sc(['teams', 'panes', 'ls', '--machine', machine ?? localMachine()], { timeout: 60_000 });
776
+ const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
777
+ if (!Array.isArray(panes)) throw new Error(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
778
+ return panes.some((p) => p.pane === pane);
779
+ }
780
+
781
+ /** Whether a pane just launched is still there after the harness had a moment to start. */
763
782
  function paneSurvives(machine, pane) {
764
783
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 6000);
765
- const r = sc(['teams', 'panes', 'ls', '--machine', machine], { timeout: 60_000 });
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
784
+ return panePresent(machine, pane);
768
785
  }
769
786
 
770
787
  /** 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.103",
3
+ "version": "0.4.104",
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": {