@volter/supercode-orchestrator 0.5.91 → 0.5.93

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 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 the agent an address names (`sc:<machine>:agent:<name>`, supercode docs/adr/0008-agent-mailbox.md),
1375
- * from its record in the machine's mail root; null for any other address. A manager named by its agent address is
1376
- * that agent's main session on the board. */
1377
- function agentMain(address) {
1378
- const name = /^sc:[^:\s]+:agent:([^:\s]+)$/.exec(address)?.[1];
1379
- if (!name) return null;
1380
- const root = process.env.SUPERCODE_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode');
1381
- try { return String(JSON.parse(readFileSync(join(root, 'mail', 'agents', `${name}.json`), 'utf8')).main_session ?? '') || null; }
1382
- catch { return null; }
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
- events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log, onEvent: (key, { uncertain = false, watched, id, board = null } = {}) => {
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);
@@ -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
- const box = mailboxOf(address);
2489
- if (!box || !existsSync(join(box, 'new'))) return;
2490
- for (const name of readdirSync(join(box, 'new')).filter((n) => n.endsWith('.json')).sort()) {
2491
- const path = join(box, 'new', name);
2492
- let envelope;
2493
- try { envelope = JSON.parse(readFileSync(path, 'utf8')); } catch { continue; }
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
- try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
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.91",
3
+ "version": "0.5.93",
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.34",
28
+ "@volter/teams": "0.2.35",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },