@volter/supercode-orchestrator 0.5.90 → 0.5.92
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 +20 -10
- package/board/dispatch.mjs +11 -20
- package/board/wake.mjs +13 -12
- package/package.json +2 -2
package/board/cli.mjs
CHANGED
|
@@ -348,6 +348,7 @@ export async function runVerb(argv) {
|
|
|
348
348
|
// a caller that is a card's own session (its harness names it; the board recorded which session it launched)
|
|
349
349
|
const listing = ['list','ls'].includes(verb);
|
|
350
350
|
await loadWorkflow(root).catch((error) => { if (verb !== 'init' && verb !== 'boards') throw error; });
|
|
351
|
+
await readAgentMains(root);
|
|
351
352
|
const sid = listing && !args.mine ? null : callerSession();
|
|
352
353
|
// what this command records is the calling session's act (its address, as its subscriptions name it)
|
|
353
354
|
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;
|
|
@@ -1371,16 +1372,22 @@ function ownCard(db, sessionId) {
|
|
|
1371
1372
|
|
|
1372
1373
|
const READS = new Set(['list', 'ls', 'show', 'context', 'runs', 'log', 'tail', 'watch', 'stats', 'assignees', 'diagnostics', 'diag', 'attachments', 'notify-list', 'capacity', 'help', 'init']);
|
|
1373
1374
|
|
|
1374
|
-
/** The main session of
|
|
1375
|
-
*
|
|
1376
|
-
* that agent's main session on the board. */
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1375
|
+
/** The main session of each agent the workflow names a manager (`sc:<machine>:agent:<name>`), asked of this machine's
|
|
1376
|
+
* daemon, which keeps agents' records (mail step 1c), once per call before its verb runs: a manager named by its agent
|
|
1377
|
+
* address is that agent's main session on the board. None for an agent the daemon does not know, or when it is down. */
|
|
1378
|
+
const agentMains = new Map();
|
|
1379
|
+
async function readAgentMains(root) {
|
|
1380
|
+
const managers = [(await loadWorkflow(root).catch(() => null))?.params?.managers ?? []].flat().map(String);
|
|
1381
|
+
for (const address of managers) {
|
|
1382
|
+
const name = /^sc:[^:\s]+:agent:([^:\s]+)$/.exec(address)?.[1];
|
|
1383
|
+
if (!name || agentMains.has(address)) continue;
|
|
1384
|
+
let main = null;
|
|
1385
|
+
try { main = (await machineCall(null, 'harness.v1.agent.show', { name, claim: { pid: process.pid, ppid: process.ppid, session: null, named: null, owner: null } }, { timeoutMs: 5_000 }))?.agents?.[0]?.main_session ?? null; }
|
|
1386
|
+
catch { /* not known: the agent is no manager's session in this call */ }
|
|
1387
|
+
agentMains.set(address, typeof main === 'string' && main ? main : null);
|
|
1388
|
+
}
|
|
1383
1389
|
}
|
|
1390
|
+
function agentMain(address) { return agentMains.get(address) ?? null; }
|
|
1384
1391
|
|
|
1385
1392
|
/**
|
|
1386
1393
|
* Who a caller is on card `cardId`, in the workflow's roles: a signed-in terminal person retains their creator role;
|
|
@@ -1665,7 +1672,10 @@ async function serve(root, args = {}) {
|
|
|
1665
1672
|
// a refresh's own reports (a configuration it could not read) never start another refresh
|
|
1666
1673
|
let refreshing = false;
|
|
1667
1674
|
const refreshNow = () => { if (refreshing) return; refreshing = true; try { events?.refresh(); } finally { refreshing = false; } };
|
|
1668
|
-
|
|
1675
|
+
// each board's mail, waited for on this machine's daemon (mail step 1c): the home's boards' own addresses
|
|
1676
|
+
const boardMailboxes = () => ['default', ...(existsSync(join(root, 'kanban', 'boards')) ? readdirSync(join(root, 'kanban', 'boards')) : [])].map((slug) => boardAddress(boardLabel(root, slug)));
|
|
1677
|
+
const mailWaiter = async () => (await machineCall(null, 'harness.v1.mail.board_wait', { boards: boardMailboxes(), timeout: 60 }, { timeoutMs: 90_000 }))?.woken ?? [];
|
|
1678
|
+
events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log, mailWaiter, onEvent: (key, { uncertain = false, watched, id, board = null } = {}) => {
|
|
1669
1679
|
if (key === 'board-registry') { retireHeldBoards(); refreshNow(); return; }
|
|
1670
1680
|
if (key === 'board-config') {
|
|
1671
1681
|
if (watched !== undefined && board) heldConfigWatched(board, id ?? key, watched);
|
package/board/dispatch.mjs
CHANGED
|
@@ -2466,18 +2466,6 @@ function fireHooks(board, settings, report) {
|
|
|
2466
2466
|
|
|
2467
2467
|
// ------------------------------------------------------------------ the board's mailbox
|
|
2468
2468
|
|
|
2469
|
-
/** The board's mailbox on this machine: the directory supercode files mail for `address` in, or null. */
|
|
2470
|
-
function mailboxOf(address) {
|
|
2471
|
-
const config = process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(process.env.HOME ?? '', '.config', 'supercode'));
|
|
2472
|
-
const root = join(config, 'mail');
|
|
2473
|
-
if (!existsSync(root)) return null;
|
|
2474
|
-
for (const dir of readdirSync(root)) {
|
|
2475
|
-
if (!dir.startsWith('board-')) continue;
|
|
2476
|
-
try { if (readFileSync(join(root, dir, 'address'), 'utf8').trim() === address) return join(root, dir); } catch { /* not a mailbox */ }
|
|
2477
|
-
}
|
|
2478
|
-
return null;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
2469
|
/**
|
|
2482
2470
|
* The board's mailbox takes answers to the questions the board asked (what a card waits on, read by the card's
|
|
2483
2471
|
* workflow) and notices about it (a machine's event, a delivery notice about a message the board sent), each filed as
|
|
@@ -2485,12 +2473,13 @@ function mailboxOf(address) {
|
|
|
2485
2473
|
* only once that refusal is filed.
|
|
2486
2474
|
*/
|
|
2487
2475
|
async function readMailbox(board, address, report) {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2476
|
+
// the board's unread mail, claimed for it by this machine's daemon, the mail folder's one reader (mail step 1c): a
|
|
2477
|
+
// daemon that does not answer leaves it unread for the next round
|
|
2478
|
+
const r = await runtimeDoor(null, 'harness.v1.mail.board_inbox', { root: board.root }, { timeout: 30_000, as: address });
|
|
2479
|
+
const answer = r.code === 0 ? lastJson(r.stdout) : null;
|
|
2480
|
+
if (!answer?.token) return;
|
|
2481
|
+
const handled = [];
|
|
2482
|
+
for (const envelope of answer.envelopes ?? []) {
|
|
2494
2483
|
const notice = envelope.kind === 'notice' || envelope.reply_via?.mode === 'none';
|
|
2495
2484
|
// mail that came through a link claims its return address (D200): the board sends nothing there on its own, its
|
|
2496
2485
|
// refusal included; the refusal is said in the board's report instead
|
|
@@ -2500,12 +2489,14 @@ async function readMailbox(board, address, report) {
|
|
|
2500
2489
|
} else if (!envelope.in_reply_to && !notice) {
|
|
2501
2490
|
try {
|
|
2502
2491
|
await fileMail(board, envelope.from, `${address} takes answers to its questions and notices only; this message was not taken.`, `board-refused-${envelope.id}`);
|
|
2503
|
-
} catch (error) { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); continue; }
|
|
2492
|
+
} catch (error) { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); continue; } // given back, unread
|
|
2504
2493
|
report.mailed.push([envelope.from, 'refused']);
|
|
2505
2494
|
}
|
|
2506
|
-
|
|
2495
|
+
handled.push(envelope.id);
|
|
2507
2496
|
if (!envelope.in_reply_to && notice) report.mailed.push([envelope.from, envelope.from === `sc:${localMachine()}:operator:teams-events` ? 'machine_event' : 'notice']);
|
|
2508
2497
|
}
|
|
2498
|
+
// what was handled is read; the rest is given back unread (a refusal that could not be filed is tried again)
|
|
2499
|
+
await runtimeDoor(null, 'harness.v1.mail.board_ack', { root: board.root, token: answer.token, ids: handled }, { timeout: 30_000, as: address });
|
|
2509
2500
|
}
|
|
2510
2501
|
|
|
2511
2502
|
// ------------------------------------------------------------------ goals and triage
|
package/board/wake.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function signalBoardWrite(root, actor) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** One coalescing queue shared by standalone serve and the hosted dispatcher. */
|
|
19
|
-
export function watchBoard({ root, binary, env, log = () => {}, onEvent = () => {} }) {
|
|
19
|
+
export function watchBoard({ root, binary, env, log = () => {}, onEvent = () => {}, mailWaiter = null }) {
|
|
20
20
|
const watchers = new Map();
|
|
21
21
|
let pending = null, waiter = null, closed = false;
|
|
22
22
|
const wake = (source, at = Date.now(), info = {}) => {
|
|
@@ -100,19 +100,20 @@ export function watchBoard({ root, binary, env, log = () => {}, onEvent = () =>
|
|
|
100
100
|
file(`${store}-wal`, 'board-storage', board);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
const config = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, 'supercode') : join(env.HOME ?? '', '.config', 'supercode'));
|
|
104
|
-
const mail = join(config, 'mail');
|
|
105
|
-
directory(mail, 'mailbox-registry', name => name.startsWith('board-'));
|
|
106
|
-
if (existsSync(mail)) for (const name of readdirSync(mail)) {
|
|
107
|
-
if (name.startsWith('board-')) {
|
|
108
|
-
const inbox = join(mail, name, 'new');
|
|
109
|
-
// Reading mail moves it out of new/. That removal is not new work and
|
|
110
|
-
// must not make the dispatcher schedule another tick for its own read.
|
|
111
|
-
directory(inbox, 'board-mail', file => file.endsWith('.json') && existsSync(join(inbox, file)));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
103
|
};
|
|
115
104
|
refresh();
|
|
105
|
+
// A board's mail arriving: waited for on this machine's daemon (mail step 1c), which keeps the mail; never by watching
|
|
106
|
+
// the mail folder. Woken, it pauses a moment, so a round that has not read it yet does not wake it again at once; a
|
|
107
|
+
// daemon that does not answer is waited on again after a while (the safety sweep covers what was missed).
|
|
108
|
+
if (mailWaiter) void (async () => {
|
|
109
|
+
while (!closed) {
|
|
110
|
+
let woken = null;
|
|
111
|
+
try { woken = await mailWaiter(); }
|
|
112
|
+
catch (error) { tell('board-mail', { uncertain: true }); log(`dispatch wait for board mail: ${error.message}; asked again in 30 s`); await new Promise(done => setTimeout(done, 30_000).unref?.()); continue; }
|
|
113
|
+
if (closed) break;
|
|
114
|
+
if (woken?.length) { wake('board-mail'); await new Promise(done => setTimeout(done, 2_000).unref?.()); }
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
116
117
|
return {
|
|
117
118
|
refresh,
|
|
118
119
|
wake,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.92",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.60",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.34",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|