@volter/supercode-orchestrator 0.5.113 → 0.5.115

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 +39 -13
  2. package/package.json +2 -2
@@ -1786,14 +1786,13 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1786
1786
  let session = null;
1787
1787
  let replaced = null;
1788
1788
  // 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.
1789
+ // session it did not launch, left running at its run's end) is never resumed beside itself: while it runs it is kept,
1790
+ // and the card's next run is delivered into it, as into any kept session.
1791
1791
  if (prev?.surface === 'native' && !prev.pane && (!prev.close_confirmed_at || prev.released)) {
1792
1792
  const reading = nativeReading(await readNative(prev.machine, prev.harness, prev.session_id), prev);
1793
1793
  if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1794
1794
  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`);
1795
+ 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
1796
  }
1798
1797
  if (prev?.forgotten) {
1799
1798
  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,11 +1806,11 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1807
1806
  } else if (prev && prev.machine === machine) {
1808
1807
  // a session the board closed, or one whose close its own command asked for (the next tick's sweep closes that
1809
1808
  // 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))) {
1809
+ const next = continuationOf(prev, prev.pane ? await panePresent(prev.machine, prev.pane) : sessionAlive(fleet, prev));
1810
+ if (next === 'refuse') throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
1811
+ if (next === 'keep') {
1813
1812
  // 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;
1813
+ 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, released: _rl, ...kept } = prev;
1815
1814
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
1816
1815
  // a kept session is told what this run is for (its card came back to it, with what happened since), only while
1817
1816
  // this run is still the card's current one (its pane and native reads were awaited)
@@ -1885,6 +1884,18 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
1885
1884
  return session;
1886
1885
  }
1887
1886
 
1887
+ /**
1888
+ * 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
1889
+ * its native process): an open session that is live is kept, and the run is delivered into it; a live pane holding a
1890
+ * session the board closed refuses the launch; anything else is resumed. A released session is open: its release only
1891
+ * freed the slot, and the session was left running.
1892
+ */
1893
+ export function continuationOf(prev, live) {
1894
+ const open = Boolean(prev.released) || (!prev.closed_at && !prev.close_requested);
1895
+ if (open) return live ? 'keep' : 'resume';
1896
+ return live && prev.pane ? 'refuse' : 'resume';
1897
+ }
1898
+
1888
1899
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1889
1900
  async function panePresent(machine, pane) {
1890
1901
  const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
@@ -2136,7 +2147,9 @@ async function adoptSession(board, id, who, { request = null, open = request, pr
2136
2147
  if (beforeRun && assignee && assignee !== task.assignee) throw new Error('rebinding a session does not reassign its active run');
2137
2148
  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
2149
  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}`);
2150
+ // a session whose close the board confirmed is not live, whatever a fleet reading still lists (a released one was
2151
+ // left running, and is read)
2152
+ 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
2153
  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
2154
  // the lane that resumes it if it is lost, set in the same transaction (assigning first would let a tick start another)
2142
2155
  if (assignee && assignee !== task.assignee) { db.prepare('UPDATE tasks SET assignee = ? WHERE id = ?').run(assignee, id); event(db, id, 'assigned', { assignee }); }
@@ -2674,17 +2687,30 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
2674
2687
  return out;
2675
2688
  }
2676
2689
 
2677
- /** Whether an arc with no open PR may be one that changed no code and is due its review: it filed and closed tasks
2678
- * (a fresh arc with none has not started), and no review of it is pending or passed (after changes are requested,
2679
- * closing the tasks that own the findings makes it due again). */
2690
+ /** Whether an arc with no open PR may be one that changed no code and is due its review: it has done work, by filing
2691
+ * tasks or by a run that ended with an Acceptance outcome ticked (a fresh arc with neither has not started), and no
2692
+ * review of it is pending or passed (after changes are requested, closing the tasks that own the findings makes it
2693
+ * due again). */
2680
2694
  function noCodeReviewDue(board, id) {
2681
2695
  return view(board, (db) => {
2682
- if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
2696
+ const filed = db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id);
2697
+ if (!filed && !(db.prepare('SELECT 1 FROM task_runs WHERE task_id = ? AND ended_at IS NOT NULL LIMIT 1').get(id) &&
2698
+ outcomeTicked(db.prepare('SELECT body FROM tasks WHERE id = ?').get(id)?.body))) return false;
2683
2699
  const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass','review_voided') ORDER BY id DESC LIMIT 1").get(id);
2684
2700
  return !last || ['changes_requested','review_voided'].includes(last.kind);
2685
2701
  });
2686
2702
  }
2687
2703
 
2704
+ /** Whether a card body ticks some outcome under its Acceptance heading. */
2705
+ function outcomeTicked(body) {
2706
+ let acceptance = false;
2707
+ return String(body ?? '').split('\n').some((line) => {
2708
+ const heading = /^#{1,6}\s+(.+?)\s*$/.exec(line);
2709
+ if (heading) acceptance = heading[1].toLowerCase() === 'acceptance';
2710
+ return acceptance && /^\s*[-*] \[[xX]\] \S/.test(line);
2711
+ });
2712
+ }
2713
+
2688
2714
  /** An opened PR asks for the declared review transition once the arc's tasks are closed. */
2689
2715
  // A task written in the card's own Tasks section that did not become a subtask holds its review too.
2690
2716
  /** The arc's session still in a turn: its review, and the close of that session the request brings (end_run), wait for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.113",
3
+ "version": "0.5.115",
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
  },