@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.
- package/board/dispatch.mjs +99 -25
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
1087
|
-
* kill and outside any write: a pane's by the harness session under its process (whatever session it is: an
|
|
1088
|
-
* never learned, a harness its record does not name), a session in no pane by its own id.
|
|
1089
|
-
* stopped, or no harness session in the pane at all, is closed
|
|
1090
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
1120
|
-
catch (error) { return `its session could not be read just now (${String(error.message).slice(0, 160)})
|
|
1121
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|