@volter/supercode-orchestrator 0.4.39 → 0.4.41

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.
@@ -539,13 +539,18 @@ async function runApply(argv) {
539
539
  return 0;
540
540
  }
541
541
 
542
+ // exit once what was written reached the pipe: a write to a pipe is asynchronous, and process.exit() right after one
543
+ // cuts the output at the pipe's buffer (a board's `list --json` past 64 KiB arrived truncated)
544
+ const flushed = (stream) => new Promise((done) => stream.write('', done));
545
+ const exitAfterFlush = async (code) => { await Promise.all([flushed(process.stdout), flushed(process.stderr)]); process.exit(code); };
546
+
542
547
  async function main() {
543
548
  const argv = process.argv.slice(2);
544
549
  if (OPERATOR_OPS.includes(argv[0])) {
545
550
  try {
546
551
  const code = await runOperator(argv[0], argv.slice(1));
547
552
  await closeWorld();
548
- process.exit(code);
553
+ await exitAfterFlush(code);
549
554
  } catch (error) {
550
555
  process.stdout.write(`${JSON.stringify({ ok: false, error: String(error.message ?? error) })}\n`);
551
556
  process.exit(1);
@@ -555,7 +560,7 @@ async function main() {
555
560
  try {
556
561
  const code = await (argv[0] === 'import' ? runImport(argv.slice(1)) : runExport(argv.slice(1)));
557
562
  await closeWorld();
558
- process.exit(code);
563
+ await exitAfterFlush(code);
559
564
  } catch (error) {
560
565
  say('error', { verb: argv[0], message: String(error.message ?? error), name: error.name ?? 'Error' });
561
566
  await closeWorld();
@@ -568,11 +573,11 @@ async function main() {
568
573
  try {
569
574
  const code = await runWorkflow(argv.slice(1));
570
575
  await closeWorld();
571
- process.exit(code);
576
+ await exitAfterFlush(code);
572
577
  } catch (error) {
573
578
  process.stderr.write(`supercode workflow: ${error.message ?? error}\n`);
574
579
  await closeWorld();
575
- process.exit(1);
580
+ await exitAfterFlush(1);
576
581
  }
577
582
  }
578
583
  if (argv[0] === 'access') {
@@ -595,10 +595,10 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
595
595
  tx(board, (db) => {
596
596
  setRunSession(db, runId, session);
597
597
  event(db, task.id, kind, { session_id: session.session_id, machine, pane: session.pane, address: session.address, mode: session.mode, ...(replaced ? { replaced } : {}) }, runId);
598
- // the working session hears updates to its card; a session no longer working it does not
599
- for (const s of subscriptions(db, task.id)) {
600
- if (s.delivery_metadata?.role === 'worker' && s.chat_id !== session.address) db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(task.id, s.chat_id);
601
- }
598
+ // the working session hears updates to its card; the slot's previous session no longer does (a reviewer starting
599
+ // leaves the implementer's, and a subtask's arc session's, subscription alone)
600
+ const before = prev ? prev.address ?? `sc:${prev.machine}:claude-code:${prev.session_id}` : null;
601
+ if (before && before !== session.address) db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(task.id, before);
602
602
  subscribeIn(db, task.id, session.address, 'worker');
603
603
  // it sees the cards its own depends on change, and works its subtasks
604
604
  for (const p of db.prepare('SELECT parent_id FROM task_links WHERE child_id = ?').all(task.id)) subscribeIn(db, p.parent_id, session.address, 'dependency');
@@ -783,11 +783,10 @@ async function notify(board, fleet, report, entry) {
783
783
  // each subscription's notices go in order; different subscribers are told side by side
784
784
  await pool(subscriptions(board.db).filter((sub) => sub.platform === 'supercode'), SEND_CONCURRENCY, async (sub) => {
785
785
  const role = ['worker', 'dependency'].includes(sub.delivery_metadata?.role) ? sub.delivery_metadata.role : 'creator';
786
- // a session that worked or watched a card and is gone hears nothing more; a creator is retried until it is back
787
- if (role !== 'creator' && fleet.ok && !fleet.alive.has(sub.chat_id)) {
788
- tx(board, (db) => db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(sub.task_id, sub.chat_id));
789
- return;
790
- }
786
+ // a session the fleet does not list this tick is told next tick: one reading is not a loss (a loaded box drops
787
+ // sessions from a read, and a dropped subscription never came back), and a session that is replaced on its card
788
+ // has its subscription moved when the new one starts
789
+ if (role !== 'creator' && fleet.ok && !fleet.alive.has(sub.chat_id)) return;
791
790
  const kinds = heardBy(entry, role);
792
791
  if (!kinds.length) return;
793
792
  const events = board.db.prepare(`SELECT id, kind, payload, created_at FROM task_events WHERE task_id = ? AND id > ? AND kind IN (${kinds.map(() => '?').join(', ')}) ORDER BY id`)
package/board/engine.mjs CHANGED
@@ -337,12 +337,12 @@ export async function act(board, id, name, { roles = null, payload = {}, extra =
337
337
  /** Take card `id` to `status` by the event the workflow says goes there for this caller (`goto`). */
338
338
  export async function goto(board, id, status, { roles = null, payload = {} } = {}) {
339
339
  const entry = await loadWorkflow(board.root);
340
- const { name, from } = tx(board, (db) => {
340
+ const { name, refusal, from } = tx(board, (db) => {
341
341
  roles ??= callerRoles(db, id);
342
- return { name: eventTo(db, board, entry, id, status, { roles, payload }), from: statusOf(db, getTask(db, id)) };
342
+ return { ...eventTo(db, board, entry, id, status, { roles, payload }), from: statusOf(db, getTask(db, id)) };
343
343
  });
344
344
  if (from === status) return { from, to: status, moved: false, event: null };
345
- if (!name) throw new Error(`${id} is ${from}; no event you may send takes it to ${status}`);
345
+ if (!name) throw new Error(refusal ?? `${id} is ${from}; no event you may send takes it to ${status}`);
346
346
  return { ...(await act(board, id, name, { roles, payload })), event: name };
347
347
  }
348
348
 
@@ -170,8 +170,9 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
170
170
 
171
171
  /**
172
172
  * The event that takes card `id` to `status` for a sender in `roles`: the first the card's status (then the workflow)
173
- * declares whose transition for that sender, with `payload`, goes there. Null when none does. Its validators are
174
- * the event's own, checked when it is sent.
173
+ * declares whose transition for that sender, with `payload`, goes there and whose validators hold. Answers
174
+ * `{ name }`, or `{ name: null, refusal }` with the first validator that stood in the way (null when no event goes
175
+ * there at all).
175
176
  */
176
177
  export function eventTo(db, board, entry, id, status, { roles = [], payload = {} } = {}) {
177
178
  const { workflow, params } = entry;
@@ -179,11 +180,15 @@ export function eventTo(db, board, entry, id, status, { roles = [], payload = {}
179
180
  const task = cardOf(db, id);
180
181
  const from = statusOf(db, task);
181
182
  const scope = scopeOf(db, board, task, { params, event: payload, extra: { lanes: lanesIn(entry), sender: roles } });
183
+ let refusal = null;
182
184
  for (const name of new Set([...Object.keys(workflow.statuses[from]?.on ?? {}), ...Object.keys(workflow.on ?? {})])) {
183
185
  const taken = transitionsFor(workflow, from, name).filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r))).find((t) => holds(t.guard ?? null, scope));
184
- if (taken?.target && String(templateValue(taken.target, scope) ?? '') === status) return name;
186
+ if (!taken?.target || String(templateValue(taken.target, scope) ?? '') !== status) continue;
187
+ const failed = (taken.validate ?? []).find((v) => !holds(v.expr, scope));
188
+ if (!failed) return { name };
189
+ refusal ??= failed.message;
185
190
  }
186
- return null;
191
+ return { name: null, refusal };
187
192
  }
188
193
 
189
194
  function cardOf(db, id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
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": {