@volter/supercode-orchestrator 0.4.65 → 0.4.67

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
@@ -10,7 +10,7 @@ import { basename, join, resolve } from 'node:path';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { read } from '../orchestration.mjs';
12
12
  import { holdDispatcherLock } from '../hermes-locks.mjs';
13
- import { backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, tx } from './store.mjs';
13
+ import { backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
14
14
  import * as engine from './engine.mjs';
15
15
  import { adoptSession, boardAddress, boardLabel, boardSettings, callerSession, lanesOf, localMachine, observeFleet, paused, plan, startCard, tick } from './dispatch.mjs';
16
16
  import { loadWorkflow, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
@@ -234,6 +234,8 @@ export async function runWorkflow(argv) {
234
234
 
235
235
  // a caller that is a card's own session (its harness names it; the board recorded which session it launched)
236
236
  const sid = callerSession();
237
+ // what this command records is the calling session's act (its address, as its subscriptions name it)
238
+ if (sid) setActor(`sc:${localMachine()}:${sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code'}:${sid}`);
237
239
  if (sid && !READS.has(verb) && boardExists(root, slug)) {
238
240
  withBoard((b) => { const own = ownCard(b.db, sid); if (own) authorize(b.db, verb, rest, args, own); });
239
241
  }
@@ -500,11 +500,6 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
500
500
 
501
501
  /** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
502
502
  export async function sessionEffect(board, e) {
503
- if (e.kind === 'tell') {
504
- // an ask is sent under the id the card's block waits on
505
- await send(boardAddress(board.label), board.label, e.to, e.text, e.messageId ?? `board-tell-${e.id}-${now()}`, { ask: Boolean(e.messageId) });
506
- return;
507
- }
508
503
  if (e.kind === 'send_to_actor') {
509
504
  const run = tx(board, (db) => currentRun(db, e.id));
510
505
  const session = run?.metadata?.supercode;
@@ -854,6 +849,25 @@ async function send(from, label, to, body, key, { queue = false, ask = false } =
854
849
  if (answer.code !== 0) throw new Error((answer.text || r.stderr || `no answer (exit ${r.code})`).trim().slice(-300));
855
850
  }
856
851
 
852
+ /**
853
+ * The messages the workflow has the board send (`tell`, `ask`: board_message events) that it has not sent yet, sent now
854
+ * and marked board_message_sent. A question goes under the id its card waits on; a resend of a sent one is a no-op
855
+ * (the id is the send's idempotency key). One that fails is tried again next tick.
856
+ */
857
+ async function sendBoardMessages(board, report) {
858
+ const rows = tx(board, (db) => db.prepare(`SELECT id, task_id, payload FROM task_events e WHERE kind = 'board_message' AND NOT EXISTS
859
+ (SELECT 1 FROM task_events s WHERE s.kind = 'board_message_sent' AND s.task_id = e.task_id AND json_extract(s.payload, '$.event') = e.id) ORDER BY id`).all());
860
+ for (const row of rows) {
861
+ const m = json(row.payload, {}) ?? {};
862
+ try {
863
+ await send(boardAddress(board.label), board.label, m.to, m.text, m.message_id ?? `board-message-${row.id}`, { ask: Boolean(m.message_id) });
864
+ tx(board, (db) => event(db, row.task_id, 'board_message_sent', { event: row.id, to: m.to, ...(m.message_id ? { message_id: m.message_id } : {}) }));
865
+ } catch (error) {
866
+ report.undelivered.push([row.task_id, m.to, String(error.message).slice(-200)]);
867
+ }
868
+ }
869
+ }
870
+
857
871
  /** The board's own address: where its notices come from and where card updates are mailed. */
858
872
  export const boardAddress = (label) => `sc:${localMachine()}:board:${label.replace(/[^A-Za-z0-9_.-]/g, '-')}`;
859
873
 
@@ -917,6 +931,8 @@ async function notify(board, fleet, report, entry) {
917
931
  try {
918
932
  for (const ev of events) {
919
933
  const p = json(ev.payload, {}) ?? {};
934
+ // nobody is told of their own act
935
+ if (p.by && p.by === sub.chat_id) { cursor = ev.id; continue; }
920
936
  let text;
921
937
  if (ev.kind === 'commented') {
922
938
  if (p.author === sub.chat_id) { cursor = ev.id; continue; }
@@ -1216,6 +1232,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
1216
1232
  supervise(board, fleet, settings, report, entry, effects); closeRequested(board, report); phase('supervise');
1217
1233
  report.unsettled = [];
1218
1234
  report.promoted = recomputeReady(board, effects, report.unsettled);
1235
+ await sendBoardMessages(board, report);
1219
1236
  const before = report.started.length;
1220
1237
  if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, hostRunning });
1221
1238
  hostRunning += report.started.length - before; phase('start');
package/board/store.mjs CHANGED
@@ -196,8 +196,14 @@ export function json(text, fallback = null) {
196
196
  }
197
197
 
198
198
  /** Append one event to a card's thread (Hermes's `task_events`); answers its id. */
199
+ // the session whose command this process runs (its address), set by the CLI: each event it records says who acted
200
+ // (`by`), so the session is not told of its own act
201
+ let actor = null;
202
+ export function setActor(address) { actor = address || null; }
203
+
199
204
  export function event(db, taskId, kind, payload = null, runId = null) {
205
+ const said = actor && !(payload && 'by' in payload) ? { ...(payload ?? {}), by: actor } : payload;
200
206
  const r = db.prepare('INSERT INTO task_events (task_id, run_id, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)')
201
- .run(taskId, runId, kind, payload ? JSON.stringify(payload) : null, now());
207
+ .run(taskId, runId, kind, said ? JSON.stringify(said) : null, now());
202
208
  return Number(r.lastInsertRowid);
203
209
  }
@@ -534,15 +534,16 @@ const ACTIONS = {
534
534
  return;
535
535
  }
536
536
  },
537
- // a message from the board to a session (`to`: its address)
538
- tell(ctx, { to, text }) { if (to) ctx.effects.push({ kind: 'tell', id: ctx.id, to: String(to), text: String(text ?? '') }); },
537
+ // a message from the board to a session (`to`: its address), kept on the card as a board_message event until the
538
+ // dispatcher has sent it: a move settled by any verb (a list settles too) is never lost with its effects
539
+ tell(ctx, { to, text }) { if (to) record(ctx.db, ctx.id, 'board_message', { to: String(to), text: String(text ?? '') }, null); },
539
540
  // a question from the board to a session, sent under a message id chosen now: `{{asked}}` in the actions after it is
540
541
  // that id (what a block waits on), and the answer to it lands in the board's mailbox
541
542
  ask(ctx, { to, text }) {
542
543
  if (!to) throw new Error(`the workflow asks nobody about ${ctx.id} (ask.to is empty)`);
543
544
  const id = `m-${randomBytes(12).toString('hex')}`;
544
545
  ctx.scope.asked = id;
545
- ctx.effects.push({ kind: 'tell', id: ctx.id, to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, messageId: id });
546
+ record(ctx.db, ctx.id, 'board_message', { to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, message_id: id }, null);
546
547
  },
547
548
  send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
548
549
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.65",
3
+ "version": "0.4.67",
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": {