@volter/supercode-orchestrator 0.4.63 → 0.4.65
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 +9 -4
- package/board/engine.mjs +7 -3
- package/board/workflow.mjs +37 -2
- 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
|
@@ -501,7 +501,8 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
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
503
|
if (e.kind === 'tell') {
|
|
504
|
-
|
|
504
|
+
// an ask is sent under the id the card's block waits on
|
|
505
|
+
await send(boardAddress(board.label), board.label, e.to, e.text, e.messageId ?? `board-tell-${e.id}-${now()}`, { ask: Boolean(e.messageId) });
|
|
505
506
|
return;
|
|
506
507
|
}
|
|
507
508
|
if (e.kind === 'send_to_actor') {
|
|
@@ -840,10 +841,11 @@ function deliverNow(board, to, body, key) {
|
|
|
840
841
|
sc(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 120_000 });
|
|
841
842
|
}
|
|
842
843
|
|
|
843
|
-
async function send(from, label, to, body, key, { queue = false } = {}) {
|
|
844
|
+
async function send(from, label, to, body, key, { queue = false, ask = false } = {}) {
|
|
844
845
|
const request = {
|
|
845
846
|
op: 'send', from, from_name: `${label} board`, to, queue, id: key,
|
|
846
|
-
|
|
847
|
+
// a question the board waits on is answered by a reply; a notice is not
|
|
848
|
+
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
849
|
};
|
|
848
850
|
// a delivery to a busy session can take a minute on a loaded machine; what it answered is the verdict (a process
|
|
849
851
|
// killed after it answered `code: 0` delivered)
|
|
@@ -1097,6 +1099,8 @@ function readMailbox(board, address, report) {
|
|
|
1097
1099
|
let envelope;
|
|
1098
1100
|
try { envelope = JSON.parse(readFileSync(path, 'utf8')); } catch { continue; }
|
|
1099
1101
|
try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
|
|
1102
|
+
// an answer to a question the board asked is what a card waits on (read by the card's workflow), not a verb
|
|
1103
|
+
if (envelope.in_reply_to) continue;
|
|
1100
1104
|
const argv = json(envelope.body, {})?.workflow;
|
|
1101
1105
|
let answer;
|
|
1102
1106
|
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 +1214,8 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
1210
1214
|
readMailbox(board, boardAddress(board.label), report); phase('mailbox');
|
|
1211
1215
|
const effects = [];
|
|
1212
1216
|
supervise(board, fleet, settings, report, entry, effects); closeRequested(board, report); phase('supervise');
|
|
1213
|
-
report.
|
|
1217
|
+
report.unsettled = [];
|
|
1218
|
+
report.promoted = recomputeReady(board, effects, report.unsettled);
|
|
1214
1219
|
const before = report.started.length;
|
|
1215
1220
|
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, hostRunning });
|
|
1216
1221
|
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,14 +84,38 @@ 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;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const OPEN_OUTCOME = /^(\s*[-*] )\[ \] (.+)$/;
|
|
95
|
+
/** Whether `body` is `was` with only outcomes ticked: each changed line was `- [ ] X` and is now `- [x] X` or
|
|
96
|
+
* `- [x] X: <pointer>`; no other line added, removed or changed. */
|
|
97
|
+
function onlyTicks(was, body) {
|
|
98
|
+
if (typeof body !== 'string') return false;
|
|
99
|
+
const before = String(was ?? '').split('\n');
|
|
100
|
+
const after = body.split('\n');
|
|
101
|
+
if (before.length !== after.length) return false;
|
|
102
|
+
return before.every((line, i) => {
|
|
103
|
+
if (line === after[i]) return true;
|
|
104
|
+
const open = OPEN_OUTCOME.exec(line);
|
|
105
|
+
if (!open) return false;
|
|
106
|
+
const ticked = `${open[1]}[x] ${open[2]}`;
|
|
107
|
+
return after[i] === ticked || (after[i].startsWith(`${ticked}: `) && after[i].length > ticked.length + 2);
|
|
108
|
+
});
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
/** The session message `id` was filed for (`supercode message show`): who was asked; or null. */
|
|
90
112
|
function askedOf(id) {
|
|
91
113
|
if (!id) return null;
|
|
92
114
|
const r = spawnSync(process.env.SUPERCODE_BIN || 'supercode', ['message', 'show', id, '--json'], { encoding: 'utf8', timeout: 20_000 });
|
|
93
|
-
|
|
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;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
/** The scope an expression is evaluated in: the card (with its relations), the event, params, time, extras. */
|
|
@@ -130,6 +155,8 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
130
155
|
// the message whose answer the standing block waits for (`block --waiting-on`), and that answer's id once it has landed
|
|
131
156
|
waiting_on: waitingOn,
|
|
132
157
|
answer: () => answerTo(waitingOn),
|
|
158
|
+
// whether an edit of its text only ticks outcome lines (the card's own session's door to its acceptance)
|
|
159
|
+
only_ticks: (body) => onlyTicks(task.body, body),
|
|
133
160
|
asked_of: () => askedOf(waitingOn),
|
|
134
161
|
last_block_event: one(db, "SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'unblocked') ORDER BY id DESC LIMIT 1", task.id)?.kind ?? null,
|
|
135
162
|
// where the latest interruption says the card resumes (any status the workflow declares); none recorded: null
|
|
@@ -509,6 +536,14 @@ const ACTIONS = {
|
|
|
509
536
|
},
|
|
510
537
|
// a message from the board to a session (`to`: its address)
|
|
511
538
|
tell(ctx, { to, text }) { if (to) ctx.effects.push({ kind: 'tell', id: ctx.id, to: String(to), text: String(text ?? '') }); },
|
|
539
|
+
// a question from the board to a session, sent under a message id chosen now: `{{asked}}` in the actions after it is
|
|
540
|
+
// that id (what a block waits on), and the answer to it lands in the board's mailbox
|
|
541
|
+
ask(ctx, { to, text }) {
|
|
542
|
+
if (!to) throw new Error(`the workflow asks nobody about ${ctx.id} (ask.to is empty)`);
|
|
543
|
+
const id = `m-${randomBytes(12).toString('hex')}`;
|
|
544
|
+
ctx.scope.asked = id;
|
|
545
|
+
ctx.effects.push({ kind: 'tell', id: ctx.id, to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, messageId: id });
|
|
546
|
+
},
|
|
512
547
|
send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
|
|
513
548
|
};
|
|
514
549
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.65",
|
|
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": {
|