@volter/supercode-orchestrator 0.5.73 → 0.5.75

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
@@ -14,12 +14,14 @@ import { taskStatusOf } from './published.mjs';
14
14
  import { createServer } from 'node:net';
15
15
  import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, copyFileSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
16
16
  import { notRuntime, ownHome } from './owner-files.mjs';
17
- import { spawnSync } from 'node:child_process';
17
+ import { execFile, spawnSync } from 'node:child_process';
18
18
  import { holdLock } from './lock-holder.mjs';
19
19
  import { takeEpoch } from './fence.mjs';
20
20
  import { dispatcherNotice, holdOwnerHealth, holdRound, holdSwitch } from './owner-health.mjs';
21
21
  import { boardTool } from './tools.mjs';
22
22
  import { recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
23
+ // read by name, so an older harness SDK without them still loads: the owner then keeps no per-verb or loop lines
24
+ import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
23
25
  import { basename, dirname, join, resolve } from 'node:path';
24
26
  import { fileURLToPath } from 'node:url';
25
27
  import { homedir, tmpdir, userInfo } from 'node:os';
@@ -28,19 +30,23 @@ import { isLocalWorkspace } from './workspace.mjs';
28
30
  import { openZtrackBoard } from './ztrack.mjs';
29
31
  import * as engine from './engine.mjs';
30
32
 
33
+ // A mailbox read through the mailbox's own command, awaited (never a wait that holds the event loop). Gap A31: a
34
+ // helper process until the daemon owns mail (order 3 PR 4).
35
+ const mailRead = (verb) => new Promise((resolve) => execFile(supercodeBin(), ['message', verb, '--json'], { encoding: 'utf8', env: commandEnv(), timeout: 15_000, maxBuffer: 64 * 1024 * 1024 }, (_, stdout) => resolve(stdout ?? '')));
36
+
31
37
  /** The messages on this machine the mailbox still has to deliver (`supercode message waiting`), by message id. */
32
- function mailWaiting() {
33
- const r = spawnSync(supercodeBin(), ['message', 'waiting', '--json'], { encoding: 'utf8', env: commandEnv(), timeout: 15_000 });
34
- try { return new Map((JSON.parse(r.stdout) ?? []).map((mail) => [mail.id, mail])); } catch { return new Map(); }
38
+ async function mailWaiting() {
39
+ const stdout = await mailRead('waiting');
40
+ try { return new Map((JSON.parse(stdout) ?? []).map((mail) => [mail.id, mail])); } catch { return new Map(); }
35
41
  }
36
42
 
37
43
  /**
38
44
  * The messages on this machine whose delivery the mailbox gave up on (`supercode message expired`), by message id:
39
45
  * filed by the board, never delivered within the mailbox's bound, kept unread. Empty when the mailbox does not answer.
40
46
  */
41
- function mailExpiries() {
42
- const r = spawnSync(supercodeBin(), ['message', 'expired', '--json'], { encoding: 'utf8', env: commandEnv(), timeout: 15_000 });
43
- try { return new Map((JSON.parse(r.stdout) ?? []).map((expiry) => [expiry.id, expiry])); } catch { return new Map(); }
47
+ async function mailExpiries() {
48
+ const stdout = await mailRead('expired');
49
+ try { return new Map((JSON.parse(stdout) ?? []).map((expiry) => [expiry.id, expiry])); } catch { return new Map(); }
44
50
  }
45
51
 
46
52
  /**
@@ -319,34 +325,6 @@ const metadataOf = (v) => {
319
325
  return got;
320
326
  };
321
327
 
322
- /**
323
- * What is wrong with this orchestrator's version, or null: a build no release names (a hand install of a branch) or one
324
- * older than npm's latest. Every workflow call says it and the dispatcher tells its managers; action is required until
325
- * the release is installed. The latest is read from the registry at most once an hour.
326
- */
327
- function releaseBehind() {
328
- let running;
329
- try { running = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; } catch { return null; }
330
- const cache = join(tmpdir(), 'supercode-orchestrator-latest.json');
331
- let latest = null;
332
- let fresh = false;
333
- try { const seen = JSON.parse(readFileSync(cache, 'utf8')); if (Date.now() - seen.at < 3_600_000) { fresh = true; latest = seen.latest; } } catch { /* read below */ }
334
- if (!fresh) {
335
- // a failed read is kept for the hour too: offline, no call waits on the registry more than once an hour
336
- const r = spawnSync('npm', ['view', '@volter/supercode-orchestrator', 'version'], { encoding: 'utf8', timeout: 5000 });
337
- latest = r.status === 0 ? r.stdout.trim() || null : null;
338
- try { writeFileSync(cache, JSON.stringify({ at: Date.now(), latest })); } catch { /* read again next call */ }
339
- }
340
- if (!latest || running === latest) return null;
341
- const parts = (v) => v.split('-')[0].split('.').map(Number);
342
- const [a, b] = [parts(running), parts(latest)];
343
- const older = a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
344
- const fix = `npm install -g @volter/supercode-orchestrator@${latest} (and each install's pinned copy)`;
345
- if (running.includes('-')) return { key: `${running}>${latest}`, text: `orchestrator ${running} is not a published release (latest ${latest}); action required: ${fix}` };
346
- if (older < 0) return { key: `${running}>${latest}`, text: `orchestrator ${running} is behind the published ${latest}; action required: ${fix}` };
347
- return null;
348
- }
349
-
350
328
  /** One verb in full, in the call it runs in (call.mjs): a caller's in the owner, or the process's own here. */
351
329
  export async function runVerb(argv) {
352
330
  await knowLocalMachine();
@@ -362,7 +340,7 @@ export async function runVerb(argv) {
362
340
  const slug = viewTag ? DEFAULT_BOARD : named;
363
341
  const open = (create = false) => { const b = openBoard(root, slug, { create, document: args.document ?? null, backing: create ? args.backing ?? null : null }); b.label = boardLabel(root, slug); return b; };
364
342
  const withBoard = (fn, create = false) => { const b = open(create); try { return fn(b); } finally { b.close(); } };
365
- const withBoardAsync = async (fn) => { const b = open(); try { return await fn(b); } finally { b.close(); } };
343
+ const withBoardAsync = async (fn, create = false) => { const b = open(create); try { return await fn(b); } finally { b.close(); } };
366
344
  const id = rest[0];
367
345
  const need = (n, what) => { if (rest.length < n) throw new Error(`${verb} needs ${what}`); };
368
346
  const ids = (first = rest) => [...first, ...(args.ids ?? [])];
@@ -380,7 +358,7 @@ export async function runVerb(argv) {
380
358
  if (sid && !READS.has(verb) && boardExists(root, slug)) {
381
359
  // binding the caller's launch is a write: only the owner, answering the caller's own verb, makes it (a local read
382
360
  // never writes)
383
- withBoard((b) => { if (isCallersCall()) reconcileCallerLaunch(b, sid); const own = ownCard(b.db, sid); if (own) authorize(b.db, verb, rest, args, own); });
361
+ await withBoardAsync(async (b) => { if (isCallersCall()) await reconcileCallerLaunch(b, sid); const own = ownCard(b.db, sid); if (own) authorize(b.db, verb, rest, args, own); });
384
362
  }
385
363
  // the board's workflow (the IR's): what each verb does, and who may send it, is its
386
364
  // every verb reads the board through its workflow (a list settles cards too), so every verb loads it
@@ -472,7 +450,7 @@ export async function runVerb(argv) {
472
450
  // --goal / --no-goal are the create's words; without either, the workflow's initial decides
473
451
  // a person filing for themselves sponsors it; an agent filing a person's request names them (proven) or records them unverified
474
452
  const sponsorship = args.sponsor || args['requested-by'] ? await requestedSponsor(args) : principalOf()?.startsWith('person:') ? { sponsor: principalOf().slice('person:'.length) } : {};
475
- const created = withBoard((b) => engine.createCard(b, {
453
+ const created = await withBoardAsync(async (b) => engine.createCard(b, await engine.prepareCard(b, {
476
454
  title: rest.join(' '), body: textFrom(args['body-file'], args.body), assignee: args.assignee === 'none' ? null : args.assignee, machine: args.machine,
477
455
  workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
478
456
  triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
@@ -482,7 +460,7 @@ export async function runVerb(argv) {
482
460
  project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : args['no-goal'] ? false : null,
483
461
  tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])],
484
462
  ...sponsorship,
485
- }), true);
463
+ })), true);
486
464
  await withBoardAsync(b=>engine.runEffects(b,created.effects??[]));
487
465
  let answer = { id: created.id, existing: created.existing };
488
466
  if (args.adopt) {
@@ -881,10 +859,9 @@ export async function runVerb(argv) {
881
859
 
882
860
  case 'diagnostics': case 'diag': {
883
861
  const { settings } = await laneState(root).catch(() => ({ settings: boardSettings({}) }));
884
- const fleet = observeFleet(root);
862
+ const fleet = await observeFleet(root);
885
863
  const publishing = await boardPublishing(root);
886
- const expiredMail = mailExpiries();
887
- const waitingMail = mailWaiting();
864
+ const [expiredMail, waitingMail] = await Promise.all([await mailExpiries(), await mailWaiting()]);
888
865
  const order = ['warning', 'error', 'critical'];
889
866
  const found = withBoard((b) => {
890
867
  const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
@@ -1146,7 +1123,7 @@ export async function runVerb(argv) {
1146
1123
  }
1147
1124
  case 'close': {
1148
1125
  need(1, 'a card id');
1149
- const closed = withBoard((b) => closeSession(b, id));
1126
+ const closed = await withBoardAsync((b) => closeSession(b, id));
1150
1127
  say(args, closed ? closed.map((c) => `closed ${c.session_id} (pane ${c.pane} on ${c.machine})`).join('\n') : `${id} has no live session`, { ok: true, closed });
1151
1128
  return 0;
1152
1129
  }
@@ -1453,13 +1430,13 @@ function authorize(db, verb, rest, args, own) {
1453
1430
  }
1454
1431
 
1455
1432
  /** Close the live sessions of a card that is not running (its worker's and its reviewers'), freeing its machine's place. */
1456
- function closeSession(board, id) {
1433
+ async function closeSession(board, id) {
1457
1434
  const task = engine.getTask(board.db, id);
1458
1435
  if (!task) throw new Error(`no card ${id}`);
1459
1436
  if (task.current_run_id != null) throw new Error(`${id} has an active run; pause, block or reclaim it before closing its session`);
1460
1437
  tx(board, (db) => requestSessionClose(db, id));
1461
1438
  const report = { undelivered: [], closed: [] };
1462
- closeRequested(board, report, { taskId: id, caller: callerSession() });
1439
+ await closeRequested(board, report, { taskId: id, caller: callerSession() });
1463
1440
  if (report.undelivered.length) throw new Error(`close remains queued for retry: ${report.undelivered.map((r) => r[2]).join('; ')}`);
1464
1441
  return report.closed.length ? report.closed : null;
1465
1442
  }
@@ -1543,8 +1520,20 @@ async function answerVerbs(root, log) {
1543
1520
  return () => { server.close(); try { unlinkSync(path); } catch { /* gone */ } lock.release(); };
1544
1521
  }
1545
1522
 
1546
- /** One request to the owner, answered whole: the verb run in the caller's call, its output kept for the answer. */
1523
+ /** One request to the owner, answered whole: the verb run in the caller's call, its output kept for the answer. A
1524
+ * verb that takes more than a second is a slow line of its own, with the steps that ended while it ran (its own and,
1525
+ * in this one process, any other call's that held it). */
1547
1526
  async function answerOne(root, line) {
1527
+ const began = performance.now();
1528
+ let named = null, by = null;
1529
+ try { return await answerRequest(root, line, (verb, caller) => { named = verb; by = caller; }); }
1530
+ finally {
1531
+ const ms = performance.now() - began;
1532
+ if (named) recordSlow({ kind: 'call', name: `owner verb ${named}`, ms, slowest: null, extra: { caller: by, steps: slowLog.stepsWithin?.(began, performance.now()) ?? null } });
1533
+ }
1534
+ }
1535
+
1536
+ async function answerRequest(root, line, naming) {
1548
1537
  try {
1549
1538
  const request = JSON.parse(line);
1550
1539
  const tool = request?.tool && typeof request.tool.name === 'string' ? request.tool : null;
@@ -1554,6 +1543,7 @@ async function answerOne(root, line) {
1554
1543
  if (BOARD_OPERATION_VERBS.has(verb)) return { code: 1, stdout: '', stderr: `supercode workflow: ${verb} is run on the board's machine as its own act, not sent to the owner\n` };
1555
1544
  const session = request.session && typeof request.session.id === 'string' && ['claude-code', 'codex'].includes(request.session.harness) ? request.session : null;
1556
1545
  const caller = typeof request.caller === 'string' ? request.caller : session ? `sc:${localMachine()}:${session.harness}:${session.id}` : null;
1546
+ naming(tool ? `tool ${tool.name}` : verb, caller);
1557
1547
  const facts = Object.fromEntries(Object.entries(request.facts ?? {}).filter(([key, value]) => CALLER_FACTS.includes(key) && typeof value === 'string'));
1558
1548
  // a caller names its Teams context (the CLI and the daemon always do): one that names none is refused before anything
1559
1549
  // is done, never answered with the owner's
@@ -1604,17 +1594,7 @@ async function serve(root, args = {}) {
1604
1594
  const FAILED_ROUNDS_ALERT = 5;
1605
1595
  const failing = new Map();
1606
1596
  let round = 0;
1607
- // A dispatcher that is not the published release is told up the chain, once per version pair: it keeps running, and
1608
- // its manager hears that action is required (a caller's stderr line alone reached no one).
1609
- const versionsTold = new Set();
1610
- const tellReleaseBehind = async () => {
1611
- const behind = releaseBehind();
1612
- if (!behind || versionsTold.has(behind.key)) return;
1613
- // the send's key makes a retry safe; a pair counts as told only once a manager got it
1614
- if (await alertManagers(root, 'default', `The dispatcher of ${root} runs ${behind.text}`, `release-behind-${behind.key}`) > 0) versionsTold.add(behind.key);
1615
- };
1616
1597
  const watchRounds = async (reports) => {
1617
- await tellReleaseBehind().catch(() => {});
1618
1598
  if (reports.every((r) => !r.error)) {
1619
1599
  // A publication the team refused holds this home's publishing where it is (nothing after it is skipped): a fact of
1620
1600
  // the same kind as rounds that stopped, so it goes where every board verb's error line reads it. Only a refusal
@@ -1660,8 +1640,13 @@ async function serve(root, args = {}) {
1660
1640
  onChange: (name) => log(`home changed (${name}): read again on its next use`),
1661
1641
  onLost: (error) => { log(`the home's files can no longer be watched (${error.message}); stopping, for the keeper to start the owner again`); stop(); },
1662
1642
  });
1643
+ // Hermes's install is located once per process (a launch's memory snapshot reads it): here, before any verb is
1644
+ // answered, so no verb or round waits on it
1645
+ try { (await import('../hermes-cli.mjs')).hermesInstall(); } catch { /* none installed */ }
1663
1646
  let closeVerbs;
1664
1647
  try { closeVerbs = await answerVerbs(root, log); } catch (error) { unfollowHome(); throw error; }
1648
+ // a held event loop is every caller's wait at once: each hold over a second is a slow line naming what held it
1649
+ const unwatchLoop = slowLog.watchEventLoop?.({ name: 'board owner event loop blocked' });
1665
1650
  log(`owning the boards of ${root} on ${localMachine()} (orchestrator ${running ?? 'unknown'}); dispatching where a board's dispatching is on (\`supercode workflow dispatching\`)`);
1666
1651
  events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log });
1667
1652
  // The board's events are published by this machine's connector, not by the dispatcher (D137, D138 step 4): the home is
@@ -1748,7 +1733,9 @@ async function serve(root, args = {}) {
1748
1733
  if (moved) { log(`orchestrator ${moved} is installed (this serve runs ${running}); stopping, for its keeper to start the new one`); break; }
1749
1734
  }
1750
1735
  } finally {
1751
- closeVerbs();
1736
+ // a fetch in flight lands, and what it brought is merged, while this owner still holds the home's lock
1737
+ try { const { settleDocuments } = await import('./git-document.mjs'); await settleDocuments(); } catch { /* nothing in flight */ }
1738
+ unwatchLoop?.(); closeVerbs();
1752
1739
  unfollowHome();
1753
1740
  await closeDoors();
1754
1741
  events?.close();