@volter/supercode-orchestrator 0.5.147 → 0.5.149

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.
Files changed (2) hide show
  1. package/board/dispatch.mjs +99 -25
  2. package/package.json +1 -1
@@ -187,9 +187,36 @@ const paneInRecord = (fleet, session) => session?.surface !== 'headless' && sess
187
187
  const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : paneInRecord(fleet, session) ?? unpublishedAlive(session);
188
188
  /** A subscriber's address as the session it names, for sessionAlive (its machine, harness and native id). */
189
189
  const sessionAt = (address) => { const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(address)); return m && HARNESSES[m[2]] ? { address, machine: normalMachine(m[1]), harness: m[2], session_id: m[3] } : null; };
190
- /** A subscriber's liveness, read first (readBefore): true, false, or null when it could not be read (unknown, never dead). */
190
+ /** An agent's address (`sc:<machine>:agent:<name>`) as its machine and name, or null. */
191
+ const agentAt = (address) => { const m = /^sc:([^:\s]+):agent:([^:\s]+)$/.exec(String(address)); return m ? { machine: normalMachine(m[1]), name: m[2] } : null; };
192
+ const agentMains = new Map();
193
+ /**
194
+ * An agent's main session, where mail to its address goes (ADR 0008), asked of its machine's daemon, which keeps the
195
+ * agent's record (one reading serves READING_MS): its address, false when that machine declares no such agent, or null
196
+ * when the daemon did not answer.
197
+ */
198
+ async function agentMain(address) {
199
+ const held = agentMains.get(address);
200
+ if (held && Date.now() - held.at <= READING_MS) return held.main;
201
+ const { machine, name } = agentAt(address);
202
+ const local = machine === localMachine();
203
+ const r = await doorOn(machine, 'harness.v1.agent.show', { name, ...(local ? { claim: { pid: process.pid, ppid: process.ppid, session: null, named: null, owner: null } } : {}) }, { timeout: 5_000 });
204
+ const answer = r.code === 0 ? lastJson(r.stdout) : null;
205
+ const main = answer?.agents?.[0]?.main_session;
206
+ const read = { at: Date.now(), main: typeof main === 'string' && main ? main : answer && Number.isInteger(answer.code) && answer.code !== 0 ? false : null };
207
+ agentMains.set(address, read);
208
+ return read.main;
209
+ }
210
+ /**
211
+ * A subscriber's liveness, read first (readBefore): true, false, or null when it could not be read (unknown, never dead).
212
+ * An agent's address is as live as its main session.
213
+ */
191
214
  async function subscriberAlive(fleet, address) {
192
215
  if (fleet.alive.has(address)) return true;
216
+ if (agentAt(address)) {
217
+ const main = await agentMain(address);
218
+ return main && sessionAt(main) ? subscriberAlive(fleet, main) : main === null ? null : false;
219
+ }
193
220
  const session = sessionAt(address);
194
221
  if (!session) return false;
195
222
  await readBefore(fleet, session);
@@ -1083,47 +1110,91 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
1083
1110
 
1084
1111
  /** Reconcile durable close intent and ended runs, including old unverified closed_at records.
1085
1112
  * Reads and process I/O stay outside the writer. An active successor owns its pane until it ends. */
1086
- /** Why the close of an ended run's live session waits, or null when nothing would be cut. Read last, just before the
1087
- * kill and outside any write: a pane's by the harness session under its process (whatever session it is: an id the run
1088
- * never learned, a harness its record does not name), a session in no pane by its own id. Only a session read idle, or
1089
- * stopped, or no harness session in the pane at all, is closed; working, waiting on input, a turn the harness does not
1090
- * publish (unknown) and a reading that fails all wait a round. The card's move never does. */
1113
+ /** Why the close of an ended run's live session waits (`why`), or null when nothing would be cut. Read last, just
1114
+ * before the kill and outside any write: a pane's by the harness session under its process (whatever session it is: an
1115
+ * id the run never learned, a harness its record does not name), a session in no pane by its own id. A session read
1116
+ * idle, or stopped, or no harness session in the pane at all, is closed. Only a turn read in progress (working, waiting
1117
+ * on input) holds the close for as long as it is read. A harness pane whose session is not found under its process is
1118
+ * read by the run's own session id, as a session in no pane is; with no row there it waits, as a reading that fails
1119
+ * does. A turn its harness does not publish (unknown) is no turn read: its close waits on the session's own record
1120
+ * instead (`unread`: since when, the record's last write then, and when it was last read), and goes ahead once the
1121
+ * board has watched `quiet` seconds with nothing written to it. Rounds further apart than `gap` are no watch: the
1122
+ * count starts again. The card's move never waits. */
1091
1123
  // The programs a harness pane runs in its foreground: Claude Code's, Codex's, and node (the daemon's Codex pane host,
1092
1124
  // a harness installed through npm). A pane running one of these runs a harness session, read or not.
1093
1125
  const HARNESS_COMMANDS = new Set(['claude', 'claude.exe', 'codex', 'codex.exe', 'node']);
1094
1126
  /** A board launch never bound to a session (no session id read for it) whose launch window has passed. */
1095
1127
  export const unboundLaunch = (session, launchWindow, at = now()) => Boolean(session?.launch_key && !session.session_id && launchWindow
1096
1128
  && Number(session.launched_at) > 0 && at - Number(session.launched_at) >= launchWindow);
1097
- async function turnAtClose(session, pane = null, { launchWindow = null } = {}) {
1129
+ /** The fleet turns of a session read in a turn (`needs_input`: a turn stopped on a permission or a question). */
1130
+ const TURN_IN_PROGRESS = new Set(['working', 'needs_input']);
1131
+ /** The watch on a session whose turn is not published: it began (`since`) at the first such reading, at the reading
1132
+ * that found its record written again (`mark`: its last write and when its harness says its state began, in ms), or
1133
+ * at the first reading after more than `gap` seconds unread (`seen`: the last reading). */
1134
+ function unreadWatch(held, mark, gap, at = now()) {
1135
+ const kept = held && Number(held.mark) === mark && Number(held.since) <= at && at - Number(held.seen) <= gap;
1136
+ return { since: kept ? Number(held.since) : at, mark, seen: at };
1137
+ }
1138
+ async function turnAtClose(session, pane = null, { launchWindow = null, quiet = null, gap = 0 } = {}) {
1098
1139
  const machine = session.machine ?? localMachine();
1099
- let activity;
1140
+ const recordOf = (rows, id, harness) => rows.find((row) => row.locator?.session_id === id && row.locator?.harness === harness);
1141
+ let activity, rows = null;
1100
1142
  if (pane?.pid) {
1101
1143
  const r = await doorOn(machine, 'harness.v1.sessions.activity_under', { pids: [pane.pid] }, { timeout: NATIVE_DISCOVERY_MS });
1102
1144
  const page = r.code === 0 ? lastJson(r.stdout) : null;
1103
- if (!Array.isArray(page?.activities)) return `the session in pane ${session.pane} could not be read just now (${(r.stderr || r.stdout).trim().slice(-160)})`;
1145
+ if (!Array.isArray(page?.activities)) return { why: `the session in pane ${session.pane} could not be read just now (${(r.stderr || r.stdout).trim().slice(-160)})` };
1104
1146
  activity = page.activities.find((item) => item.pid === pane.pid)?.activity ?? null;
1105
1147
  // No session read under the pane is no absence (a registry it could not read, another config home, a failed process
1106
- // read): the pane's own foreground program says whether a harness runs there. Only a pane running none is closed.
1148
+ // read, a process walk cut short): the pane's own foreground program says whether a harness runs there. A pane
1149
+ // running none is closed; one running a harness is read by the session id the run holds.
1107
1150
  if (!activity) {
1108
1151
  // A pane the board launched whose session no reading ever found (the fleet's never bound it, and none runs under
1109
1152
  // its process now) past the launch window holds a harness that never started a session: nothing is cut by its
1110
1153
  // close. Waiting on it kept t_7ec85b6b's Codex pane on aarons-mac-mini, stuck before its first turn, open for 15 h.
1111
1154
  if (unboundLaunch(session, launchWindow)) return null;
1112
1155
  const command = String(pane.command ?? '').split(/[\\/]/).pop().toLowerCase();
1113
- return command && !HARNESS_COMMANDS.has(command) ? null : `pane ${session.pane} runs ${command || 'a program it does not name'}, whose session could not be read`;
1156
+ if (command && !HARNESS_COMMANDS.has(command)) return null;
1157
+ const unfound = `pane ${session.pane} runs ${command || 'a program it does not name'}, whose session could not be read`;
1158
+ if (!session.session_id) return { why: unfound };
1159
+ try { rows = (await discoverNative(machine, session.harness ?? 'claude-code', session.session_id)).sessions; }
1160
+ catch (error) { return { why: `${unfound} (${String(error.message).slice(0, 160)})` }; }
1161
+ activity = recordOf(rows, session.session_id, session.harness ?? 'claude-code')?.activity ?? null;
1162
+ // a row that shows no live owner beside a pane that runs a harness places nothing: another session may run there
1163
+ if (activity?.presence !== 'running') return { why: unfound };
1114
1164
  }
1115
1165
  } else if (pane) {
1116
- return `pane ${session.pane} names no process to read`;
1166
+ return { why: `pane ${session.pane} names no process to read` };
1117
1167
  } else {
1118
- if (!session.session_id) return 'its session is not known yet';
1119
- try { activity = nativeReading((await discoverNative(machine, session.harness ?? 'claude-code', session.session_id)).sessions, session).row?.activity ?? null; }
1120
- catch (error) { return `its session could not be read just now (${String(error.message).slice(0, 160)})`; }
1121
- if (!activity) return `its session ${addressOf(session)} is not in its machine's discovery`;
1168
+ if (!session.session_id) return { why: 'its session is not known yet' };
1169
+ try { rows = (await discoverNative(machine, session.harness ?? 'claude-code', session.session_id)).sessions; }
1170
+ catch (error) { return { why: `its session could not be read just now (${String(error.message).slice(0, 160)})` }; }
1171
+ activity = nativeReading(rows, session).row?.activity ?? null;
1172
+ if (!activity) return { why: `its session ${addressOf(session)} is not in its machine's discovery` };
1122
1173
  }
1123
1174
  if (activity.presence && activity.presence !== 'running') return null;
1124
1175
  if (activity.turn === 'idle') return null;
1125
1176
  const who = activity.session_id ? `sc:${normalMachine(machine)}:${activity.harness ?? session.harness}:${activity.session_id}` : addressOf(session);
1126
- return `its session ${who} is in a turn (${activity.turn ?? 'unknown'}); it closes once the turn ends`;
1177
+ const inTurn = (turn) => ({ why: `its session ${who} is in a turn (${turn}); it closes once the turn ends`, unread: null });
1178
+ if (TURN_IN_PROGRESS.has(activity.turn)) return inTurn(activity.turn);
1179
+ // No turn published: the session's own record says whether anything goes on in it. One whose record does not answer,
1180
+ // or shows neither a write nor when its state began, has nothing to count from and waits.
1181
+ const unknown = `its session ${who} publishes no turn (${activity.turn ?? 'unknown'})`;
1182
+ const sessionId = activity.session_id ?? session.session_id, harness = activity.harness ?? session.harness ?? 'claude-code';
1183
+ if (!sessionId || !quiet) return { why: unknown };
1184
+ if (!rows) try { rows = (await discoverNative(machine, harness, sessionId)).sessions; }
1185
+ catch (error) { return { why: `${unknown}, and its record could not be read just now (${String(error.message).slice(0, 160)})` }; }
1186
+ const record = recordOf(rows, sessionId, harness);
1187
+ if (TURN_IN_PROGRESS.has(record?.activity?.turn)) return inTurn(record.activity.turn);
1188
+ const mark = Math.max(Number(record?.updated_at_ms) || 0, Number(activity.evidence?.native_state_since_ms) || 0);
1189
+ if (!mark) return { why: `${unknown}, and its record shows no write to count from` };
1190
+ const unread = unreadWatch(session.close_unread, mark, gap);
1191
+ if (unread.seen - unread.since >= quiet) return null;
1192
+ return { why: `${unknown}; it closes once it is read idle, or after ${quiet}s watched with nothing written to its record`, unread };
1193
+ }
1194
+ /** Hold the close of an ended run's session for what `turnAtClose` read, with the watch that reading leaves on the run. */
1195
+ async function holdForTurn(session, pane, options) {
1196
+ const held = await turnAtClose(session, pane, options);
1197
+ if (held) throw Object.assign(new Error(`close deferred: ${held.why}`), 'unread' in held ? { closeUnread: held.unread } : {});
1127
1198
  }
1128
1199
 
1129
1200
  /** The closes this process is making now, by run: each one's outcome, for a pass that meets it under way. */
@@ -1230,7 +1301,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1230
1301
  }
1231
1302
  let settle;
1232
1303
  closesUnderway.set(flight, new Promise((resolve) => { settle = resolve; }));
1233
- let error = null, closed = false;
1304
+ let error = null, closed = false, unread;
1234
1305
  try {
1235
1306
  const claimed = tx(board, (db) => {
1236
1307
  const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
@@ -1250,7 +1321,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1250
1321
  if (reading.alive) {
1251
1322
  const protectedNow = active.some(r => addressOf(json(r.metadata, {})?.supercode) === addressOf(session));
1252
1323
  if (protectedNow) throw new Error('close deferred: native session is owned by an unended run');
1253
- if (row.ended_at) { const inTurn = await turnAtClose(session); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
1324
+ if (row.ended_at) { const { idleQuiet: quiet, interval } = boardSettings(workflowFor(board.root).params); await holdForTurn(session, null, { quiet, gap: Math.max(3 * interval, quiet / 2) }); }
1254
1325
  await stopSession(board, session);
1255
1326
  }
1256
1327
  } else if (!finalFailure) {
@@ -1267,7 +1338,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1267
1338
  if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
1268
1339
  // An ended run's session is closed only between its turns, read last, after the claim and the pane list (a pane
1269
1340
  // already gone is settled above with no reading; t_f34710bc's coder was cut mid-proof at 15:01:13Z, 2026-10-04).
1270
- if (row.ended_at) { const inTurn = await turnAtClose(session, observed.live.get(session.pane), { launchWindow: boardSettings(workflowFor(board.root).params).launchWindow }); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
1341
+ if (row.ended_at) { const { launchWindow, idleQuiet: quiet, interval } = boardSettings(workflowFor(board.root).params); await holdForTurn(session, observed.live.get(session.pane), { launchWindow, quiet, gap: Math.max(3 * interval, quiet / 2) }); }
1271
1342
  await stopSession(board, session); observed.live.delete(session.pane);
1272
1343
  }
1273
1344
  }
@@ -1276,6 +1347,8 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1276
1347
  // the prefix is decided on the whole message, before it is cut to size
1277
1348
  const message = String(e.message), deferred = Boolean(e.launchDeferred) || /^close deferred: /.test(message);
1278
1349
  error = `${deferred ? 'close deferred: ' : ''}${message.replace(/^close deferred: /, '').slice(-480)}`;
1350
+ // the watch a turn reading left (or ended: a turn read in progress); an attempt that took none keeps the last
1351
+ if ('closeUnread' in e) unread = e.closeUnread;
1279
1352
  }
1280
1353
  tx(board, (db) => {
1281
1354
  const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
@@ -1285,14 +1358,14 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
1285
1358
  if (error && current.close_confirmed_at) return;
1286
1359
  if (error) {
1287
1360
  const final = !/^close deferred/.test(error);
1288
- setRunSession(db, row.id, { ...current, close_attempted_at: at, close_attempts: Number(current.close_attempts ?? 0) + 1, close_error: error, ...(final ? { close_failed_at: at } : {}) });
1361
+ setRunSession(db, row.id, { ...current, close_attempted_at: at, close_attempts: Number(current.close_attempts ?? 0) + 1, close_error: error, ...(final ? { close_failed_at: at } : {}), ...(unread !== undefined ? { close_unread: unread } : {}) });
1289
1362
  // Retain every attempt's latest fact, but avoid an unbounded thread of identical failures.
1290
1363
  if (current.close_error !== error) event(db, row.task_id, 'session_close_failed', { session_id: session.session_id, pane: session.pane, error, retry: !final }, row.id);
1291
1364
  if (final && current.close_error !== error) event(db, row.task_id, 'run_incident', { observation: 'close_failed', run: row.id, reason: `the board could not close ${addressOf(session)} and will not try again: ${error}`, session: session.session_id, machine: session.machine, pane: session.pane ?? null, evidence: { error } }, row.id);
1292
1365
  } else if (!current.close_confirmed_at) {
1293
1366
  // a close another closer already confirmed (a claim that expired under it) is said once, by that closer
1294
1367
  const { close_error: _error, ...rest } = current;
1295
- setRunSession(db, row.id, { ...rest, close_error: null, closed_at: at, close_confirmed_at: at });
1368
+ setRunSession(db, row.id, { ...rest, close_error: null, close_unread: null, closed_at: at, close_confirmed_at: at });
1296
1369
  const address = addressOf(session);
1297
1370
  // The closed session stops hearing every card it heard as their worker: its own and each subtask (a launch
1298
1371
  // subscribes it to all of them), unless another unended run still works in it, read in this transaction.
@@ -2032,7 +2105,7 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
2032
2105
  if (next === 'refuse') throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
2033
2106
  if (next === 'keep') {
2034
2107
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
2035
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_claim: _cl, monitor_held: _mh, released: _rl, ...kept } = prev;
2108
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_unread: _cu, close_claim: _cl, monitor_held: _mh, released: _rl, ...kept } = prev;
2036
2109
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
2037
2110
  // a kept session is told what this run is for (its card came back to it, with what happened since), only while
2038
2111
  // this run is still the card's current one (its pane and native reads were awaited)
@@ -2704,8 +2777,9 @@ async function notify(board, fleet, report, entry) {
2704
2777
  // the card as the workflow names it (its status the workflow's: triaging, reviewing, stopped; `stored_as` kanban.db's)
2705
2778
  const task = view(board, (db) => { const t = getTask(db, sub.task_id); return t ? { ...t, status: statusOf(db, t), stored_as: t.status } : t; });
2706
2779
  // a worker whose run has ended is not woken by a notice (a second session would start working the card beside
2707
- // the one the next run gets): the notice is filed for its next turn
2708
- const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
2780
+ // the one the next run gets): the notice is filed for its next turn, and the next run's brief carries the card's
2781
+ // thread. A subtask has no run of its own: its worker works in its arc's run.
2782
+ const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, task?.subtask_of ?? sub.task_id)?.metadata?.supercode) === sub.chat_id);
2709
2783
  // a Claude session always starts a turn when mail reaches it, so its mail is always delivered now
2710
2784
  const wake = String(sub.chat_id).includes(':claude-code:') || (working && !(!entry.workflow.run && sub.delivery_mode === 'notify'));
2711
2785
  const where = "WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.147",
3
+ "version": "0.5.149",
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": {