@volter/supercode-orchestrator 0.5.35 → 0.5.36

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
@@ -15,7 +15,7 @@ import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath,
15
15
  import { isLocalWorkspace } from './workspace.mjs';
16
16
  import { openZtrackBoard } from './ztrack.mjs';
17
17
  import * as engine from './engine.mjs';
18
- import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, supercodeBin, tick } from './dispatch.mjs';
18
+ import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick } from './dispatch.mjs';
19
19
  import { watchBoard } from './wake.mjs';
20
20
  import { streamBoardPublications } from './event-stream.mjs';
21
21
  import { completionOwnerOf, isLatestReviewer, loadWorkflow, requestSessionClose, scopeOf, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
@@ -1379,9 +1379,9 @@ async function serve(root, args = {}) {
1379
1379
  if (args.pidfile) writeFileSync(args.pidfile, `${process.pid}\n`);
1380
1380
  let stopped = false;
1381
1381
  let events;
1382
- const stop = () => { stopped = true; events?.close(); };
1383
- process.on('SIGTERM', stop);
1384
- process.on('SIGINT', stop);
1382
+ // a signal ends the round in progress with nothing new started (dispatch.mjs stopDispatching) and no round after it
1383
+ const stop = () => { stopped = true; stopDispatching(); events?.close(); };
1384
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, stop);
1385
1385
  // what the serve says is kept in the home too (logs/workflow-serve.log), so why it stopped outlives its pane
1386
1386
  const kept = join(root, 'logs', 'workflow-serve.log');
1387
1387
  const log = (text) => {
@@ -1488,7 +1488,7 @@ async function serve(root, args = {}) {
1488
1488
  interval = settings.interval;
1489
1489
  const reports = await tick({ root, lanes: state.lanes, settings, params, deploymentReceipt: true });
1490
1490
  round += 1;
1491
- await watchRounds(reports);
1491
+ if (!stopped) await watchRounds(reports);
1492
1492
  for (const r of reports) {
1493
1493
  nextMailAt = Math.min(nextMailAt, r.next_mail_at ?? Infinity);
1494
1494
  const said = summary(r);
@@ -1509,7 +1509,7 @@ async function serve(root, args = {}) {
1509
1509
  }
1510
1510
  } catch (error) {
1511
1511
  log(`tick failed: ${error.message}`);
1512
- await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
1512
+ if (!stopped) await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
1513
1513
  }
1514
1514
  // each round is a line of its own in the slow log, with the steps it spent its time on
1515
1515
  recordSlow({ kind: 'call', name: 'dispatch round', ms: Date.now() - began, slowest: null, extra: { steps: takeSteps() } });
@@ -1529,7 +1529,7 @@ async function serve(root, args = {}) {
1529
1529
  }
1530
1530
  } finally {
1531
1531
  events?.close();
1532
- process.off('SIGTERM', stop); process.off('SIGINT', stop);
1532
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.off(signal, stop);
1533
1533
  if (args.pidfile) { try { unlinkSync(args.pidfile); } catch { /* gone */ } }
1534
1534
  }
1535
1535
  log(stopped ? 'stopped (a signal)' : 'stopped');
@@ -2009,6 +2009,7 @@ export async function sendBoardMessages(board, report, taskId = null) {
2009
2009
  const planned = tx(board, db => pendingRequestMail(db, taskId));
2010
2010
  if (Number.isFinite(planned.nextAt)) report.next_mail_at = Math.min(report.next_mail_at ?? Infinity, planned.nextAt);
2011
2011
  for (const mail of planned.ready) {
2012
+ if (stopping) break;
2012
2013
  const first = mail.members[0];
2013
2014
  try {
2014
2015
  const task = view(board, db => getTask(db, first.task_id));
@@ -2189,7 +2190,7 @@ async function notify(board, fleet, report, entry) {
2189
2190
  } else text = noticeOf(entry, role, ev.kind, task, p);
2190
2191
  if (text) {
2191
2192
  const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
2192
- if (refused.has(sub.chat_id)) { untried = true; break; }
2193
+ if (refused.has(sub.chat_id) || stopping) { untried = true; break; }
2193
2194
  let receipt;
2194
2195
  try {
2195
2196
  receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: String(sub.chat_id).includes(':claude-code:') ? false : !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
@@ -2454,6 +2455,11 @@ function scAsync(args, { input = null, timeout = 120_000 } = {}) {
2454
2455
  });
2455
2456
  }
2456
2457
 
2458
+ // A dispatcher told to stop (its serve's SIGTERM, SIGINT or SIGHUP) starts nothing more in the round it is in: no further
2459
+ // board, card start, board message or notice. What it leaves untried is not a failure; the next dispatcher sends it.
2460
+ let stopping = false;
2461
+ export function stopDispatching() { stopping = true; }
2462
+
2457
2463
  // how many deliveries a tick has in flight at once (each is a `supercode message` process of a few seconds)
2458
2464
  const SEND_CONCURRENCY = 8;
2459
2465
 
@@ -2461,7 +2467,7 @@ const SEND_CONCURRENCY = 8;
2461
2467
  async function pool(items, n, fn) {
2462
2468
  let next = 0;
2463
2469
  await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
2464
- while (next < items.length) { const i = next; next += 1; await fn(items[i]); }
2470
+ while (next < items.length && !stopping) { const i = next; next += 1; await fn(items[i]); }
2465
2471
  }));
2466
2472
  }
2467
2473
 
@@ -2598,6 +2604,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2598
2604
  const held = dispatch ? homeHoldings(root, boards, lanes, fleet, entry) : [];
2599
2605
  const out = [];
2600
2606
  for (const slug of boards) {
2607
+ if (stopping) break;
2601
2608
  const report = { board: slug, ms: { fleet: fleetMs }, mailed: [], started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
2602
2609
  // the mailbox's answers in flight (not part of the report the caller prints)
2603
2610
  Object.defineProperty(report, 'answers', { value: [], enumerable: false });
@@ -2628,7 +2635,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2628
2635
  tx(board,db=>auditEndedRuns(db,board,entry));
2629
2636
  if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
2630
2637
  await sendBoardMessages(board, report);
2631
- if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
2638
+ if (dispatch && !stopping) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
2632
2639
  phase('start');
2633
2640
  if (dispatch && !only && !report.paused) observeIdle(board, fleet, settings, report, entry, effects);
2634
2641
  // what the moves asked for, once they are committed (messages to actors, closed panes, cleaned workspaces)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
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": {