@volter/supercode-orchestrator 0.5.72 → 0.5.73

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
@@ -17,6 +17,7 @@ import { notRuntime, ownHome } from './owner-files.mjs';
17
17
  import { spawnSync } from 'node:child_process';
18
18
  import { holdLock } from './lock-holder.mjs';
19
19
  import { takeEpoch } from './fence.mjs';
20
+ import { dispatcherNotice, holdOwnerHealth, holdRound, holdSwitch } from './owner-health.mjs';
20
21
  import { boardTool } from './tools.mjs';
21
22
  import { recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
22
23
  import { basename, dirname, join, resolve } from 'node:path';
@@ -374,6 +375,8 @@ export async function runVerb(argv) {
374
375
  current().recorded = sid && boardExists(root, slug) ? withBoard((b) => b.db.prepare("SELECT json_extract(metadata, '$.supercode.address') AS address FROM task_runs WHERE json_extract(metadata, '$.supercode.session_id') = ? ORDER BY id DESC LIMIT 1").get(sid)?.address) ?? null : null;
375
376
  if (sid) setActor(callerAddress());
376
377
  setPrincipal(await callerPrincipal(root, sid, { teams: !sid && !READS.has(verb) }));
378
+ // the dispatcher as the owner holds it, said once per call (owner-health.mjs; nothing is read for it)
379
+ if (!current().said.has(`dispatcher:${slug}`)) { current().said.add(`dispatcher:${slug}`); for (const line of dispatcherNotice(slug)) current().err(`${line}\n`); }
377
380
  if (sid && !READS.has(verb) && boardExists(root, slug)) {
378
381
  // binding the caller's launch is a write: only the owner, answering the caller's own verb, makes it (a local read
379
382
  // never writes)
@@ -1487,6 +1490,7 @@ function setDispatching(root, slug, on, reason) {
1487
1490
  db.prepare("INSERT INTO supercode_board_state (key, value) VALUES ('dispatching', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(JSON.stringify(value));
1488
1491
  });
1489
1492
  } finally { board.close(); }
1493
+ holdSwitch(slug, value);
1490
1494
  return value;
1491
1495
  }
1492
1496
 
@@ -1508,6 +1512,10 @@ async function answerVerbs(root, log) {
1508
1512
  ownHome(home);
1509
1513
  // the lock held, this owner takes its fencing epoch and fences every board, before it answers anything (fence.mjs)
1510
1514
  const epoch = takeEpoch(root, () => listBoards(root).map((slug) => openBoard(root, slug)));
1515
+ // what the owner holds of its dispatcher from here on: each board's switch, and its predecessor's last round, read once
1516
+ let lastRound = null;
1517
+ try { const beat = JSON.parse(readFileSync(join(root, 'dispatcher-heartbeat.json'), 'utf8')); lastRound = { at: beat.at, errors: beat.errors ?? [] }; } catch { /* no round recorded yet */ }
1518
+ holdOwnerHealth(listBoards(root).map((slug) => [slug, dispatchingOf(root, slug)]), lastRound);
1511
1519
  log(`owner epoch ${epoch}`);
1512
1520
  const path = boardOwnerSocket(root);
1513
1521
  try { unlinkSync(path); } catch { /* none left */ }
@@ -1617,7 +1625,9 @@ async function serve(root, args = {}) {
1617
1625
  const heldLine = refused ? [`publishing to the team is held: the team refused ${refused.card}'s ${refused.kind} (sequence ${refused.sequence}) with ${refused.status}${refused.code ? ` ${refused.code}` : ''}: ${String(refused.error ?? '').split('\n')[0].slice(0, 200)} (tried ${refused.attempts} time(s)); nothing of this home after it is published until the team takes it`] : [];
1618
1626
  const temporary = `${heartbeat}.${process.pid}.tmp`;
1619
1627
  try {
1620
- writeFileSync(temporary, `${JSON.stringify({ at: new Date().toISOString(), round, pid: process.pid, version: installed(), boards: reports.map((r) => ({ board: r.board, started: r.started?.length ?? 0, queued: r.queued?.length ?? 0, failed: r.failed?.length ?? 0, ms: r.ms ?? null })), errors: [...heldLine, ...reports.flatMap((r) => [...(r.failed ?? []), ...(r.unsettled ?? [])].map(([t, why]) => `${r.board} ${t}: ${String(why).split('\n')[0].slice(0, 300)}`))].slice(0, 20) })}\n`);
1628
+ const beat = { at: new Date().toISOString(), round, pid: process.pid, version: installed(), boards: reports.map((r) => ({ board: r.board, started: r.started?.length ?? 0, queued: r.queued?.length ?? 0, failed: r.failed?.length ?? 0, ms: r.ms ?? null })), errors: [...heldLine, ...reports.flatMap((r) => [...(r.failed ?? []), ...(r.unsettled ?? [])].map(([t, why]) => `${r.board} ${t}: ${String(why).split('\n')[0].slice(0, 300)}`))].slice(0, 20) };
1629
+ holdRound(beat.at, beat.errors);
1630
+ writeFileSync(temporary, `${JSON.stringify(beat)}\n`);
1621
1631
  renameSync(temporary, heartbeat);
1622
1632
  } catch { /* the next round writes it */ }
1623
1633
  }
@@ -0,0 +1,30 @@
1
+ // What the board owner knows of its own dispatcher, held in its memory: each board's dispatching switch (loaded once
2
+ // when the owner starts, then set by the owner's own switch writes) and its last completed round (set as each round
3
+ // completes). A verb's notice is formatted from this; nothing is read per call. Outside the owner nothing is held.
4
+ const switches = new Map(); // board slug → { on, at, by, reason }
5
+ let round = null; // { at, errors }
6
+ let holding = false;
7
+
8
+ /** The owner starts holding: each board's switch as stored, and the last round its predecessor recorded. */
9
+ export function holdOwnerHealth(boards, lastRound) {
10
+ holding = true;
11
+ for (const [slug, state] of boards) switches.set(slug, state);
12
+ round = lastRound;
13
+ }
14
+ /** The owner wrote a board's switch. */
15
+ export function holdSwitch(slug, state) { if (holding) switches.set(slug, state); }
16
+ /** The owner completed a round. */
17
+ export function holdRound(at, errors) { if (holding) round = { at, errors }; }
18
+
19
+ /** The notice for a verb on `slug`: dispatching off (a state), or a stalled round and its standing errors (failures). */
20
+ export function dispatcherNotice(slug) {
21
+ if (!holding) return [];
22
+ const state = switches.get(slug);
23
+ if (!state) return [];
24
+ if (!state.on) return [`board: dispatching is off on ${slug}${state.at ? ` since ${String(state.at).slice(0, 16)}Z` : ''}${state.by ? ` by ${state.by}` : ''}${state.reason ? ` (${state.reason})` : ''}; no round runs until \`supercode workflow dispatching on\``];
25
+ if (!round) return [];
26
+ return [
27
+ ...(Date.now() - Date.parse(round.at) > 300_000 ? [`board: error: the dispatcher has completed no round since ${String(round.at).slice(0, 16)}Z; nothing starts, moves or is supervised`] : []),
28
+ ...(round.errors ?? []).map((error) => `board: error: ${error}`),
29
+ ];
30
+ }
package/board/ztrack.mjs CHANGED
@@ -104,17 +104,6 @@ class ZtrackBoard {
104
104
  // A push younger than a minute is still on its way.
105
105
  if (late && Date.now() - late.since > 60_000) current().err(`board: warning: ${late.commits} board document commit${late.commits === 1 ? '' : 's'} not published since ${new Date(late.since).toISOString().slice(0, 16)}Z${late.error ? ` (last push: ${late.error})` : ''}; the dispatcher's next round retries\n`);
106
106
  }
107
- // The dispatcher's standing errors are said the same way, on every workflow call until a round clears them: a round
108
- // that has not completed in five minutes, and each card or run the last round could not start, read or sync.
109
- if (root && !said.has(`heartbeat:${root}`)) {
110
- said.add(`heartbeat:${root}`);
111
- try {
112
- const beat = JSON.parse(readFileSync(join(root, 'dispatcher-heartbeat.json'), 'utf8'));
113
- const age = Date.now() - Date.parse(beat.at);
114
- if (age > 300_000) current().err(`board: error: the dispatcher has completed no round since ${String(beat.at).slice(0, 16)}Z; nothing starts, moves or is supervised\n`);
115
- for (const error of beat.errors ?? []) current().err(`board: error: ${error}\n`);
116
- } catch { /* a board no dispatcher serves yet has no heartbeat */ }
117
- }
118
107
  this.seed = seed; this.root = root; this.slug = slug; this.init = init;
119
108
  this.file = `${marker}.board.db`;
120
109
  mkdirSync(dirname(this.path), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.72",
3
+ "version": "0.5.73",
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": {