@volter/supercode-orchestrator 0.5.71 → 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 +13 -5
- 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)
|
|
@@ -449,13 +452,11 @@ export async function runVerb(argv) {
|
|
|
449
452
|
}
|
|
450
453
|
// its dispatching switch, set before any owner runs (an install's first start: `init --dispatching off`); while an
|
|
451
454
|
// owner runs, `workflow dispatching` is its verb
|
|
452
|
-
|
|
453
|
-
setDispatching(root, slug, args.dispatching === 'on', 'set at init');
|
|
454
|
-
}
|
|
455
|
+
const switched = args.dispatching !== undefined ? setDispatching(root, slug, args.dispatching === 'on', 'set at init') : null;
|
|
455
456
|
const backing = made.backing;
|
|
456
457
|
made.close();
|
|
457
458
|
const { profiles } = await laneState(root).catch(() => ({ profiles: [] }));
|
|
458
|
-
say(args, `${backing === 'sqlite' ? 'Kanban DB' : `Board (${backing} backing)`} ${kept ? 'already' : 'initialized'} at ${boardPath(root, slug)}\n\n${profiles.length ? `Profiles of this home (any can be an --assignee): ${profiles.join(', ')}` : 'No profiles in this home.'}\nThe dispatcher: \`supercode workflow serve\`, or the orchestrator while it serves the home
|
|
459
|
+
say(args, `${backing === 'sqlite' ? 'Kanban DB' : `Board (${backing} backing)`} ${kept ? 'already' : 'initialized'} at ${boardPath(root, slug)}\n\n${profiles.length ? `Profiles of this home (any can be an --assignee): ${profiles.join(', ')}` : 'No profiles in this home.'}\nThe dispatcher: \`supercode workflow serve\`, or the orchestrator while it serves the home.${switched ? `\nBoard ${slug}: dispatching ${switched.on ? 'on' : 'off'} (set at init).` : ''}`, { ok: true, board: slug, path: boardPath(root, slug), backing, kept: Boolean(kept), profiles, ...(switched ? { dispatching: switched } : {}) });
|
|
459
460
|
return 0;
|
|
460
461
|
}
|
|
461
462
|
|
|
@@ -1489,6 +1490,7 @@ function setDispatching(root, slug, on, reason) {
|
|
|
1489
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));
|
|
1490
1491
|
});
|
|
1491
1492
|
} finally { board.close(); }
|
|
1493
|
+
holdSwitch(slug, value);
|
|
1492
1494
|
return value;
|
|
1493
1495
|
}
|
|
1494
1496
|
|
|
@@ -1510,6 +1512,10 @@ async function answerVerbs(root, log) {
|
|
|
1510
1512
|
ownHome(home);
|
|
1511
1513
|
// the lock held, this owner takes its fencing epoch and fences every board, before it answers anything (fence.mjs)
|
|
1512
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);
|
|
1513
1519
|
log(`owner epoch ${epoch}`);
|
|
1514
1520
|
const path = boardOwnerSocket(root);
|
|
1515
1521
|
try { unlinkSync(path); } catch { /* none left */ }
|
|
@@ -1619,7 +1625,9 @@ async function serve(root, args = {}) {
|
|
|
1619
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`] : [];
|
|
1620
1626
|
const temporary = `${heartbeat}.${process.pid}.tmp`;
|
|
1621
1627
|
try {
|
|
1622
|
-
|
|
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`);
|
|
1623
1631
|
renameSync(temporary, heartbeat);
|
|
1624
1632
|
} catch { /* the next round writes it */ }
|
|
1625
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": {
|