@volter/supercode-orchestrator 0.5.114 → 0.5.116

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 +36 -12
  2. package/package.json +2 -2
@@ -172,6 +172,8 @@ const HARNESSES = {
172
172
  // machine_offline; a stream that timed out, 504), says nothing about what it was asked: it is tried again next round.
173
173
  const doorTransient = (r) => r.code === null || ['unavailable', 'machine_offline', 'rate_limited', 504, '504', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN'].includes(json(String(r.stderr ?? '').trim().split('\n').pop(), null)?.code);
174
174
  const adoptedOnly = (s) => s?.mode === 'adopt' && !s.launch_key && s.surface !== 'headless';
175
+ /** A close the machine refused for good (not deferred): the board will not try it again, and the session runs on. */
176
+ const closeRefused = (s) => Boolean(s?.close_failed_at && s.close_error && !/^close deferred/.test(s.close_error) && !s.close_confirmed_at);
175
177
  const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
176
178
 
177
179
  // A session in a live pane of its machine is the fleet's reading; any other run is read on its own machine: a headless
@@ -1059,7 +1061,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1059
1061
  }
1060
1062
  // A final refusal stops further close attempts. Only an authorized observation of this exact pane may establish
1061
1063
  // absence: panes.list can be filtered or carry missing context keys. Unknown/invisible keeps the original failure.
1062
- const finalFailure = Boolean(session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error));
1064
+ const finalFailure = closeRefused(session);
1063
1065
  if (finalFailure) {
1064
1066
  if (!session.pane) continue;
1065
1067
  const r = await doorOn(session.machine ?? localMachine(), 'harness.v1.panes.presence', { pane: session.pane }, { timeout: 10_000, as: boardAddress(board.label) });
@@ -1724,9 +1726,13 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1724
1726
  // session starts (never two processes on one session)
1725
1727
  // Every old pane in the slot must be settled, including a failed launch with no session id.
1726
1728
  await closeRequested(board, { undelivered: [] }, { taskId: task.id });
1729
+ // The slot's own last session whose close was refused for good is not waited on: it runs on, and is continued below
1730
+ // (continuationOf), never started beside.
1731
+ const last = view(board, (db) => lastSession(db, task.id, slot));
1727
1732
  const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
1728
1733
  .map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
1729
- .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);
1734
+ .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
1735
+ && !(closeRefused(r.session) && last?.session_id && r.session.session_id === last.session_id));
1730
1736
  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'}`);
1731
1737
  let prev = view(board, (db) => lastSession(db, task.id, slot));
1732
1738
  if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
@@ -1740,7 +1746,7 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1740
1746
  if (pane && pane !== prev.pane) prev = { ...prev, pane, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
1741
1747
  fleet = fresh;
1742
1748
  }
1743
- if (prev?.close_requested && !prev.closed_at) {
1749
+ if (prev?.close_requested && !prev.closed_at && !closeRefused(prev)) {
1744
1750
  const report = { undelivered: [] };
1745
1751
  await closeRequested(board, report, { taskId: task.id });
1746
1752
  prev = view(board, (db) => lastSession(db, task.id, slot));
@@ -1786,14 +1792,13 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1786
1792
  let session = null;
1787
1793
  let replaced = null;
1788
1794
  // A session in no pane is read on its machine before anything replaces it. One the board released (an adopted
1789
- // session it did not launch, left running at its run's end) is its owner's while it runs: it is never resumed beside
1790
- // itself, and the launch waits for the owner to end it.
1795
+ // session it did not launch, left running at its run's end) is never resumed beside itself: while it runs it is kept,
1796
+ // and the card's next run is delivered into it, as into any kept session.
1791
1797
  if (prev?.surface === 'native' && !prev.pane && (!prev.close_confirmed_at || prev.released)) {
1792
1798
  const reading = nativeReading(await readNative(prev.machine, prev.harness, prev.session_id), prev);
1793
1799
  if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1794
1800
  fleet.native ??= new Map(); fleet.native.set(addressOf(prev), reading);
1795
- if (reading.alive && prev.released) throw unavailable(`released session ${addressOf(prev)} still runs; the launch waits for its owner to end it`);
1796
- 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`);
1801
+ if (reading.alive && !prev.released && (prev.closed_at || prev.close_requested || prev.forgotten)) throw unavailable(`native session ${addressOf(prev)} is still live; its close must finish before replacement`);
1797
1802
  }
1798
1803
  if (prev?.forgotten) {
1799
1804
  if (!prev.close_confirmed_at && prev.pane && await panePresent(prev.machine, prev.pane)) throw new Error(`forgotten session ${prev.session_id} still has live pane ${prev.pane}; close it before replacement`);
@@ -1807,17 +1812,22 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1807
1812
  } else if (prev && prev.machine === machine) {
1808
1813
  // a session the board closed, or one whose close its own command asked for (the next tick's sweep closes that
1809
1814
  // run's pane), is not kept, whatever a fleet reading taken before the close still lists
1810
- const paneLive = prev.pane ? await panePresent(prev.machine, prev.pane) : false;
1811
- 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`);
1812
- if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
1815
+ const next = continuationOf(prev, prev.pane ? await panePresent(prev.machine, prev.pane) : sessionAlive(fleet, prev));
1816
+ if (next === 'refuse') throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
1817
+ if (next === 'keep') {
1813
1818
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
1814
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, monitor_held: _mh, ...kept } = prev;
1819
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_claim: _cl, monitor_held: _mh, released: _rl, ...kept } = prev;
1815
1820
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
1816
1821
  // a kept session is told what this run is for (its card came back to it, with what happened since), only while
1817
1822
  // this run is still the card's current one (its pane and native reads were awaited)
1818
1823
  tx(board, (db) => {
1819
1824
  if (!runCurrent(db, task.id, runId)) throw Object.assign(new Error(`run ${runId} of ${task.id} ended or was replaced while its session was read; nothing was told to it`), { launchSuperseded: true });
1820
1825
  setRunSession(db, runId, { ...session, ...meta });
1826
+ // an earlier run's close of this pane is this run's now: the pane is the kept session's, not a close to retry
1827
+ if (prev.pane) for (const r of db.prepare('SELECT id, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId)) {
1828
+ const s = json(r.metadata, {})?.supercode;
1829
+ if (s?.pane === prev.pane && s.machine === prev.machine && !s.close_confirmed_at && !s.close_transferred_to) setRunSession(db, r.id, { ...s, close_transferred_to: runId, close_requested: null });
1830
+ }
1821
1831
  });
1822
1832
  // filed in its mailbox, which delivers it
1823
1833
  if (input) { assertRunCurrent(board, task.id, runId); await fileMail(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`); }
@@ -1885,6 +1895,18 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1885
1895
  return session;
1886
1896
  }
1887
1897
 
1898
+ /**
1899
+ * What a slot's last session on the card's machine is to the card's next run, given whether it is live (its pane, else
1900
+ * its native process): an open session that is live is kept, and the run is delivered into it; a live pane holding a
1901
+ * session the board closed refuses the launch; anything else is resumed. A released session is open: its release only
1902
+ * freed the slot, and the session was left running. So is one whose close the machine refused for good.
1903
+ */
1904
+ export function continuationOf(prev, live) {
1905
+ const open = Boolean(prev.released) || closeRefused(prev) || (!prev.closed_at && !prev.close_requested);
1906
+ if (open) return live ? 'keep' : 'resume';
1907
+ return live && prev.pane ? 'refuse' : 'resume';
1908
+ }
1909
+
1888
1910
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1889
1911
  async function panePresent(machine, pane) {
1890
1912
  const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
@@ -2136,7 +2158,9 @@ async function adoptSession(board, id, who, { request = null, open = request, pr
2136
2158
  if (beforeRun && assignee && assignee !== task.assignee) throw new Error('rebinding a session does not reassign its active run');
2137
2159
  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`);
2138
2160
  const was = lastSession(db, id);
2139
- if (was && was.session_id !== sessionId && sessionAlive(fleet, was)) throw new Error(`${id} already has a live session, ${was.address}`);
2161
+ // a session whose close the board confirmed is not live, whatever a fleet reading still lists (a released one was
2162
+ // left running, and is read)
2163
+ if (was && was.session_id !== sessionId && !(was.close_confirmed_at && !was.released) && sessionAlive(fleet, was)) throw new Error(`${id} already has a live session, ${was.address}`);
2140
2164
  if (!task.machine) db.prepare('INSERT INTO supercode_cards (task_id, machine) VALUES (?, ?) ON CONFLICT(task_id) DO UPDATE SET machine = excluded.machine').run(id, host);
2141
2165
  // the lane that resumes it if it is lost, set in the same transaction (assigning first would let a tick start another)
2142
2166
  if (assignee && assignee !== task.assignee) { db.prepare('UPDATE tasks SET assignee = ? WHERE id = ?').run(assignee, id); event(db, id, 'assigned', { assignee }); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.114",
3
+ "version": "0.5.116",
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": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@volter/supercode-harness-sdk": "^0.3.60",
28
- "@volter/teams": "0.2.46",
28
+ "@volter/teams": "0.2.47",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },