@volter/supercode-orchestrator 0.4.64 → 0.4.66
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 +1 -0
- package/board/dispatch.mjs +27 -7
- package/board/engine.mjs +7 -3
- package/board/workflow.mjs +21 -4
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -1133,6 +1133,7 @@ async function serve(root, args = {}) {
|
|
|
1133
1133
|
stuck[r.board] = r.ready_waiting && !r.started.length && !r.paused ? (stuck[r.board] ?? 0) + 1 : 0;
|
|
1134
1134
|
if (stuck[r.board] >= 6 && Date.now() - (warned[r.board] ?? 0) >= 300_000) { log(`board ${r.board} stuck: ${r.ready_waiting} ready card(s) and nothing started for ${stuck[r.board]} ticks; \`supercode workflow diagnostics\``); warned[r.board] = Date.now(); }
|
|
1135
1135
|
for (const [t, why] of r.failed) log(` ${t} did not start: ${why}`);
|
|
1136
|
+
for (const [t, why] of r.unsettled ?? []) log(` ${t} could not be read: ${why}`);
|
|
1136
1137
|
for (const [t, s, m] of r.started) log(` ${t} started ${s} (${m})`);
|
|
1137
1138
|
for (const [t, to, why] of r.undelivered) log(` notice for ${t} to ${to} not delivered: ${why}`);
|
|
1138
1139
|
}
|
package/board/dispatch.mjs
CHANGED
|
@@ -500,10 +500,6 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
500
500
|
|
|
501
501
|
/** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
|
|
502
502
|
export async function sessionEffect(board, e) {
|
|
503
|
-
if (e.kind === 'tell') {
|
|
504
|
-
await send(boardAddress(board.label), board.label, e.to, e.text, `board-tell-${e.id}-${now()}`);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
503
|
if (e.kind === 'send_to_actor') {
|
|
508
504
|
const run = tx(board, (db) => currentRun(db, e.id));
|
|
509
505
|
const session = run?.metadata?.supercode;
|
|
@@ -840,10 +836,11 @@ function deliverNow(board, to, body, key) {
|
|
|
840
836
|
sc(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 120_000 });
|
|
841
837
|
}
|
|
842
838
|
|
|
843
|
-
async function send(from, label, to, body, key, { queue = false } = {}) {
|
|
839
|
+
async function send(from, label, to, body, key, { queue = false, ask = false } = {}) {
|
|
844
840
|
const request = {
|
|
845
841
|
op: 'send', from, from_name: `${label} board`, to, queue, id: key,
|
|
846
|
-
|
|
842
|
+
// a question the board waits on is answered by a reply; a notice is not
|
|
843
|
+
body: ask ? `[A question from the ${label} board] ${body}` : `[Board notice from the ${label} board] ${body}\n\n(An automated notice from the board's dispatcher; replies to this address go nowhere.)`,
|
|
847
844
|
};
|
|
848
845
|
// a delivery to a busy session can take a minute on a loaded machine; what it answered is the verdict (a process
|
|
849
846
|
// killed after it answered `code: 0` delivered)
|
|
@@ -852,6 +849,25 @@ async function send(from, label, to, body, key, { queue = false } = {}) {
|
|
|
852
849
|
if (answer.code !== 0) throw new Error((answer.text || r.stderr || `no answer (exit ${r.code})`).trim().slice(-300));
|
|
853
850
|
}
|
|
854
851
|
|
|
852
|
+
/**
|
|
853
|
+
* The messages the workflow has the board send (`tell`, `ask`: board_message events) that it has not sent yet, sent now
|
|
854
|
+
* and marked board_message_sent. A question goes under the id its card waits on; a resend of a sent one is a no-op
|
|
855
|
+
* (the id is the send's idempotency key). One that fails is tried again next tick.
|
|
856
|
+
*/
|
|
857
|
+
async function sendBoardMessages(board, report) {
|
|
858
|
+
const rows = tx(board, (db) => db.prepare(`SELECT id, task_id, payload FROM task_events e WHERE kind = 'board_message' AND NOT EXISTS
|
|
859
|
+
(SELECT 1 FROM task_events s WHERE s.kind = 'board_message_sent' AND s.task_id = e.task_id AND json_extract(s.payload, '$.event') = e.id) ORDER BY id`).all());
|
|
860
|
+
for (const row of rows) {
|
|
861
|
+
const m = json(row.payload, {}) ?? {};
|
|
862
|
+
try {
|
|
863
|
+
await send(boardAddress(board.label), board.label, m.to, m.text, m.message_id ?? `board-message-${row.id}`, { ask: Boolean(m.message_id) });
|
|
864
|
+
tx(board, (db) => event(db, row.task_id, 'board_message_sent', { event: row.id, to: m.to, ...(m.message_id ? { message_id: m.message_id } : {}) }));
|
|
865
|
+
} catch (error) {
|
|
866
|
+
report.undelivered.push([row.task_id, m.to, String(error.message).slice(-200)]);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
855
871
|
/** The board's own address: where its notices come from and where card updates are mailed. */
|
|
856
872
|
export const boardAddress = (label) => `sc:${localMachine()}:board:${label.replace(/[^A-Za-z0-9_.-]/g, '-')}`;
|
|
857
873
|
|
|
@@ -1097,6 +1113,8 @@ function readMailbox(board, address, report) {
|
|
|
1097
1113
|
let envelope;
|
|
1098
1114
|
try { envelope = JSON.parse(readFileSync(path, 'utf8')); } catch { continue; }
|
|
1099
1115
|
try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
|
|
1116
|
+
// an answer to a question the board asked is what a card waits on (read by the card's workflow), not a verb
|
|
1117
|
+
if (envelope.in_reply_to) continue;
|
|
1100
1118
|
const argv = json(envelope.body, {})?.workflow;
|
|
1101
1119
|
let answer;
|
|
1102
1120
|
if (!Array.isArray(argv) || !argv.length || NOT_BY_MAIL.has(argv[0])) answer = `not applied: a mailed card update is one \`supercode workflow\` verb other than ${[...NOT_BY_MAIL].join(', ')}`;
|
|
@@ -1210,7 +1228,9 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
1210
1228
|
readMailbox(board, boardAddress(board.label), report); phase('mailbox');
|
|
1211
1229
|
const effects = [];
|
|
1212
1230
|
supervise(board, fleet, settings, report, entry, effects); closeRequested(board, report); phase('supervise');
|
|
1213
|
-
report.
|
|
1231
|
+
report.unsettled = [];
|
|
1232
|
+
report.promoted = recomputeReady(board, effects, report.unsettled);
|
|
1233
|
+
await sendBoardMessages(board, report);
|
|
1214
1234
|
const before = report.started.length;
|
|
1215
1235
|
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, hostRunning });
|
|
1216
1236
|
hostRunning += report.started.length - before; phase('start');
|
package/board/engine.mjs
CHANGED
|
@@ -367,14 +367,18 @@ function stopWorker(board, runId) {
|
|
|
367
367
|
if (worker?.surface === 'headless' && worker.pid) terminate(Number(worker.pid));
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
/**
|
|
371
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Every card whose workflow status has an `always` or due `after` move that holds takes it (the tick's recompute).
|
|
372
|
+
* What the moves ask for (a message, a closed pane) joins `effects`; a card whose moves could not be read (a door its
|
|
373
|
+
* expressions call failed) is left where it is and named in `errors`. Answers how many moved.
|
|
374
|
+
*/
|
|
375
|
+
export function recomputeReady(board, effects = [], errors = []) {
|
|
372
376
|
const entry = workflowFor(board.root);
|
|
373
377
|
return tx(board, (db) => {
|
|
374
378
|
let moved = 0;
|
|
375
379
|
for (const { id } of all(db, "SELECT id FROM tasks WHERE status NOT IN ('done', 'archived')")) {
|
|
376
380
|
const before = statusOf(db, getTask(db, id));
|
|
377
|
-
settle(db, board, entry, id);
|
|
381
|
+
try { settle(db, board, entry, id, effects); } catch (error) { errors.push([id, String(error.message).slice(-300)]); continue; }
|
|
378
382
|
if (statusOf(db, getTask(db, id)) !== before) moved += 1;
|
|
379
383
|
}
|
|
380
384
|
return moved;
|
package/board/workflow.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// the engine's primitives (the rows Hermes's tables hold); the I/O an action asks for (stopping a worker, closing a
|
|
6
6
|
// pane, sending a session a message, cleaning a workspace) is answered as effects the caller runs after commit.
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
8
9
|
import { event as record, json, now } from './store.mjs';
|
|
9
10
|
import { evaluate, holds, render } from './expr.mjs';
|
|
10
11
|
|
|
@@ -83,7 +84,11 @@ function lastCommitAt(path, id) {
|
|
|
83
84
|
function answerTo(id) {
|
|
84
85
|
if (!id) return null;
|
|
85
86
|
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['message', 'show', '--replies', id, '--json'], { encoding: 'utf8', timeout: 20_000 });
|
|
86
|
-
|
|
87
|
+
let rows = null;
|
|
88
|
+
try { rows = JSON.parse(r.stdout); } catch { /* read below */ }
|
|
89
|
+
// a door that cannot answer is said, never read as "no answer yet": a wait would silently never end
|
|
90
|
+
if (!Array.isArray(rows)) throw new Error(`the answer to ${id} could not be read (supercode message show --replies): ${(r.stderr || r.stdout || r.error?.message || `exit ${r.status}`).trim().slice(-200)}`);
|
|
91
|
+
return rows[0]?.replies?.[0]?.id ?? null;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
const OPEN_OUTCOME = /^(\s*[-*] )\[ \] (.+)$/;
|
|
@@ -107,7 +112,10 @@ function onlyTicks(was, body) {
|
|
|
107
112
|
function askedOf(id) {
|
|
108
113
|
if (!id) return null;
|
|
109
114
|
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['message', 'show', id, '--json'], { encoding: 'utf8', timeout: 20_000 });
|
|
110
|
-
|
|
115
|
+
let rows = null;
|
|
116
|
+
try { rows = JSON.parse(r.stdout); } catch { /* read below */ }
|
|
117
|
+
if (!Array.isArray(rows)) throw new Error(`who ${id} was sent to could not be read (supercode message show): ${(r.stderr || r.stdout || r.error?.message || `exit ${r.status}`).trim().slice(-200)}`);
|
|
118
|
+
return rows[0]?.message?.session ?? null;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/** The scope an expression is evaluated in: the card (with its relations), the event, params, time, extras. */
|
|
@@ -526,8 +534,17 @@ const ACTIONS = {
|
|
|
526
534
|
return;
|
|
527
535
|
}
|
|
528
536
|
},
|
|
529
|
-
// a message from the board to a session (`to`: its address)
|
|
530
|
-
|
|
537
|
+
// a message from the board to a session (`to`: its address), kept on the card as a board_message event until the
|
|
538
|
+
// dispatcher has sent it: a move settled by any verb (a list settles too) is never lost with its effects
|
|
539
|
+
tell(ctx, { to, text }) { if (to) record(ctx.db, ctx.id, 'board_message', { to: String(to), text: String(text ?? '') }, null); },
|
|
540
|
+
// a question from the board to a session, sent under a message id chosen now: `{{asked}}` in the actions after it is
|
|
541
|
+
// that id (what a block waits on), and the answer to it lands in the board's mailbox
|
|
542
|
+
ask(ctx, { to, text }) {
|
|
543
|
+
if (!to) throw new Error(`the workflow asks nobody about ${ctx.id} (ask.to is empty)`);
|
|
544
|
+
const id = `m-${randomBytes(12).toString('hex')}`;
|
|
545
|
+
ctx.scope.asked = id;
|
|
546
|
+
record(ctx.db, ctx.id, 'board_message', { to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, message_id: id }, null);
|
|
547
|
+
},
|
|
531
548
|
send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
|
|
532
549
|
};
|
|
533
550
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.66",
|
|
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": {
|