@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 +11 -1
- package/board/owner-health.mjs +30 -0
- package/board/ztrack.mjs +0 -11
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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": {
|