@volter/supercode-orchestrator 0.5.32 → 0.5.34

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/bin/runtime.mjs CHANGED
@@ -674,6 +674,15 @@ async function main() {
674
674
  return;
675
675
  }
676
676
  const root = resolve(args.root ?? defaultRoot());
677
+ // `SUPERCODE_BOARD_DISPATCHER=serve`: whoever started this daemon also runs `workflow serve` on the home, the board's
678
+ // one dispatcher, so this daemon runs no dispatch round (board.mjs). It is this process's alone: the sessions it
679
+ // launches do not inherit it. Any other value is refused before this daemon takes its lease or a lock.
680
+ const boardDispatcher = process.env.SUPERCODE_BOARD_DISPATCHER || null;
681
+ delete process.env.SUPERCODE_BOARD_DISPATCHER;
682
+ if (boardDispatcher !== null && boardDispatcher !== 'serve') {
683
+ say('error', { root, message: `SUPERCODE_BOARD_DISPATCHER is \`${boardDispatcher}\`: the one value it takes is \`serve\` (a \`workflow serve\` beside this daemon dispatches the board); unset, the board workflow's dispatch.serve decides` });
684
+ process.exit(2);
685
+ }
677
686
  let crashed = false;
678
687
  let gatewayLock = null;
679
688
  if (!args.once) {
@@ -747,6 +756,7 @@ async function main() {
747
756
  // build's, not whatever `supercode` a PATH lookup happens to find.
748
757
  const client = new SupercodeHarnessClient(process.env.SUPERCODE_BIN ? { command: process.env.SUPERCODE_BIN } : {});
749
758
  const orchestrator = new Orchestrator({
759
+ boardDispatcher,
750
760
  executeJob: args.executorUrl ? httpJobExecutor({ url: args.executorUrl, tokenFile: args.executorTokenFile, root }) : null,
751
761
  state,
752
762
  vault,
package/board/cli.mjs CHANGED
@@ -1444,6 +1444,8 @@ async function serve(root, args = {}) {
1444
1444
  const stuck = {};
1445
1445
  const warned = {};
1446
1446
  let pausedSaid = false;
1447
+ // a board whose cards name an assignee that is no profile of this home is said once, until it has none
1448
+ const laneSaid = new Set();
1447
1449
  let wakeReason = null;
1448
1450
  try {
1449
1451
  while (!stopped) {
@@ -1498,6 +1500,8 @@ async function serve(root, args = {}) {
1498
1500
  stuck[r.board] = r.ready_waiting && !r.started.length && !r.paused ? (stuck[r.board] || Date.now()) : 0;
1499
1501
  if (stuck[r.board] && Date.now() - stuck[r.board] >= interval * 6000 && Date.now() - (warned[r.board] ?? 0) >= 300_000) { log(`board ${r.board} stuck: ${r.ready_waiting} ready card(s) and nothing started for ${Math.floor((Date.now() - stuck[r.board]) / 1000)}s; \`supercode workflow diagnostics\``); warned[r.board] = Date.now(); }
1500
1502
  for (const [t, why] of r.queued) log(` ${t} waits: ${why}`);
1503
+ if (r.nonspawnable?.length && !laneSaid.has(r.board)) { laneSaid.add(r.board); log(`board ${r.board}: ${r.nonspawnable.length} card(s) name an assignee that is no profile of this home, and wait`); }
1504
+ if (!r.nonspawnable?.length) laneSaid.delete(r.board);
1501
1505
  for (const [t, why] of r.failed) log(` ${t} did not start: ${why}`);
1502
1506
  for (const [t, why] of r.unsettled ?? []) log(` ${t} could not be read: ${why}`);
1503
1507
  for (const [t, s, m] of r.started) log(` ${t} started ${s} (${m})`);
package/board.mjs CHANGED
@@ -3,6 +3,10 @@
3
3
  // format and lifecycle, each profile's lane decided by its `worker` in the IR. Where a Hermes gateway would run
4
4
  // its dispatcher, the daemon runs this one the same way: when the board's workflow says so (its `dispatch.serve`),
5
5
  // taking no file locks, waking on events with `dispatch.tick` as a safety sweep.
6
+ // One dispatcher per board holds by construction, not by a lock: whoever starts the daemon beside a
7
+ // `supercode workflow serve` on the same home (an Open Autonomy install's start) says so (`dispatcher: 'serve'`, from
8
+ // SUPERCODE_BOARD_DISPATCHER), and the daemon then runs no round and no board event relay of its own. Chat-platform
9
+ // subscriptions are delivered here whoever dispatches: the daemon hosts the channel adapters and a serve hosts none.
6
10
  // Each tick runs as
7
11
  // `workflow dispatch` in a child process: a tick waits on supercode (the fleet, launches, mail), and the daemon's
8
12
  // own loop never does.
@@ -32,13 +36,38 @@ function dispatchOnce(root) {
32
36
  });
33
37
  }
34
38
 
35
- /** Start the board's dispatcher for the home at `root`; answers `{ tick, stop }`, or null when it does not dispatch. */
36
- export async function startBoard({ root, orchestration = null, log = () => {}, adapters = {}, wake = null }) {
39
+ /**
40
+ * The chat-platform notices alone, for a daemon that does not dispatch the board: each sweep sends what the board's
41
+ * subscriptions have not yet been told through this daemon's channel adapters. Null when there is nothing it could send.
42
+ */
43
+ function startChatNotices({ root, entry, seconds, log, adapters, wake }) {
44
+ if (!Object.keys(entry.workflow.notify?.chat ?? {}).length) return null;
45
+ if (!Object.keys(adapters).length) { log('warn', 'board: the board workflow tells chats (notify.chat) and this daemon hosts no channel adapter to tell them'); return null; }
46
+ let running = false;
47
+ const sweep = async () => {
48
+ if (running) return;
49
+ running = true;
50
+ try { await notifyChats(root, adapters, log, wake); } catch (error) { log('warn', `board: chat notices failed: ${error.message}`); } finally { running = false; }
51
+ };
52
+ const timer = setInterval(() => { void sweep(); }, seconds * 1000);
53
+ timer.unref?.();
54
+ void sweep();
55
+ log('info', `board: chat notices every ${seconds}s through this daemon's channel adapters`);
56
+ return { tick() { void sweep(); }, stop() { clearInterval(timer); } };
57
+ }
58
+
59
+ /**
60
+ * Start the board's dispatcher for the home at `root`; answers `{ tick, stop }`, or null when it neither dispatches
61
+ * nor has chat notices to send. `dispatcher: 'serve'` says a `supercode workflow serve` dispatches this home's board:
62
+ * the daemon then runs no round of its own, only the chat notices its adapters deliver.
63
+ */
64
+ export async function startBoard({ root, orchestration = null, log = () => {}, adapters = {}, wake = null, dispatcher = null }) {
37
65
  // whether and how often the served home dispatches is the board workflow's (`dispatch.serve`, `dispatch.tick`)
38
66
  let entry;
39
67
  try { entry = workflowEntry(orchestration); } catch (error) { log('warn', `board: not dispatched: ${error.message}`); return null; }
40
- if (!holds(entry.workflow.dispatch.serve ?? null, { params: entry.params })) { log('info', 'board: not dispatched (the board workflow\'s dispatch.serve)'); return null; }
41
68
  const seconds = Math.max(Number(evaluate(entry.workflow.dispatch.tick, { params: entry.params })) || 60, 1);
69
+ if (dispatcher === 'serve') { log('info', 'board: not dispatched here (its dispatcher is the `workflow serve` beside this daemon)'); return startChatNotices({ root, entry, seconds, log, adapters, wake }); }
70
+ if (!holds(entry.workflow.dispatch.serve ?? null, { params: entry.params })) { log('info', 'board: not dispatched (the board workflow\'s dispatch.serve)'); return startChatNotices({ root, entry, seconds, log, adapters, wake }); }
42
71
  let running = false;
43
72
  let stopped = false;
44
73
  const said = new Set();
package/loop.mjs CHANGED
@@ -54,8 +54,10 @@ export class Orchestrator {
54
54
  * @param {(profile, worker, env) => object|undefined} [o.launchFor] launch override per worker (tests)
55
55
  * @param {(level, message) => void} [o.log]
56
56
  */
57
- constructor({ state, vault = new Map(), client, adapters = {}, clock = null, launchFor = null, executeJob = null, log = () => {} }) {
57
+ constructor({ state, vault = new Map(), client, adapters = {}, clock = null, launchFor = null, executeJob = null, boardDispatcher = null, log = () => {} }) {
58
58
  this.state = state;
59
+ // who dispatches the home's board: 'serve' when a `workflow serve` beside this daemon does (board.mjs)
60
+ this.boardDispatcher = boardDispatcher;
59
61
  this.executeJob = executeJob;
60
62
  this.hostedTasks = new Set();
61
63
  this.identityRecord = new SenderIdentityRecord();
@@ -96,7 +98,7 @@ export class Orchestrator {
96
98
  if (this.autoTick) { this.timer = setInterval(() => { this.tick().catch(() => undefined); }, TICK_MS); this.timer.unref?.(); }
97
99
  // the home's board, dispatched as a Hermes gateway dispatches it (docs/plans/hermes-compat.md row 14)
98
100
  if (this.autoTick) this.board = await startBoard({
99
- root: this.state.root, orchestration: this.state, log: this.log, adapters: this.adapters,
101
+ root: this.state.root, orchestration: this.state, log: this.log, adapters: this.adapters, dispatcher: this.boardDispatcher,
100
102
  // a board notice as a turn in the subscribed chat's own session (Hermes's wake), sent as the subscriber
101
103
  wake: (sub, text) => this.dispatchLogged(events.inbound({
102
104
  profile: sub.notifier_profile || 'default', key: { platform: sub.platform, kind: sub.chat_type || 'dm', chat_id: sub.chat_id, ...(sub.thread_id ? { thread_id: sub.thread_id } : {}) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
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": {