@volter/supercode-orchestrator 0.5.118 → 0.5.120
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/ci.mjs +15 -2
- package/board/dispatch.mjs +43 -11
- package/board/store.mjs +10 -1
- package/board/ztrack.mjs +4 -2
- package/package.json +1 -1
package/board/ci.mjs
CHANGED
|
@@ -145,9 +145,22 @@ async function repositoryWorkspace(board, id) {
|
|
|
145
145
|
// Resolution without create names a solo worktree even before it exists on
|
|
146
146
|
// this machine. Its portable repository and branch still identify the
|
|
147
147
|
// pushed PR. Only an actual directory can supply local source evidence.
|
|
148
|
-
return { ...resolved, here:
|
|
148
|
+
if (here && !checkoutPresent(resolved.path)) return { ...resolved, here: await recreatedWorktree(workspace, resolved) };
|
|
149
|
+
return { ...resolved, here };
|
|
149
150
|
}
|
|
150
|
-
return workspace.workspace_path ? { path: workspace.workspace_path, here } : null;
|
|
151
|
+
return workspace.workspace_path ? { path: workspace.workspace_path, here: here && checkoutPresent(workspace.workspace_path) } : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A card's declared solo worktree that is missing here (deleted, or never made on this machine) while its branch is on
|
|
155
|
+
* the remote is made again from the remote branch (the daemon's resolve with create), so its review reads the pushed
|
|
156
|
+
* head instead of waiting on a checkout nobody will restore. Answers whether the checkout is now present. */
|
|
157
|
+
async function recreatedWorktree(workspace, resolved) {
|
|
158
|
+
if (workspace.workspace_kind !== 'worktree' || !resolved?.branch || !resolved.root || !checkoutPresent(resolved.root)) return false;
|
|
159
|
+
let pushed = '';
|
|
160
|
+
try { pushed = await run('git', ['ls-remote', '--heads', 'origin', `refs/heads/${resolved.branch}`], resolved.root); } catch { return false; }
|
|
161
|
+
if (!pushed) return false;
|
|
162
|
+
// a worktree that cannot be made here leaves the card reviewed at its pushed head, as a card on another machine is
|
|
163
|
+
try { const made = await resolveWorkspace(workspace, true); return made.path === resolved.path && checkoutPresent(made.path); } catch { return false; }
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
const pullRequestLists = new Map();
|
package/board/dispatch.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import { homedir, hostname, tmpdir, totalmem } from 'node:os';
|
|
|
39
39
|
import { basename, dirname, join } from 'node:path';
|
|
40
40
|
import { createHash, randomUUID } from 'node:crypto';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
|
-
import { view, boardCopyOf, boardPath, claimer, claimHolds, copyRefusal, event, json, listBoards, logsDir, newClaim, now, openBoard, setActor, tx } from './store.mjs';
|
|
42
|
+
import { view, boardCopyOf, boardPath, claimer, claimHolds, copyRefusal, event, json, listBoards, logsDir, newClaim, now, openBoard, setActor, storageFault, tx } from './store.mjs';
|
|
43
43
|
import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, projectStatus, storedStatus, templateValue, workflowFor } from './workflow.mjs';
|
|
44
44
|
import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
|
|
45
45
|
import { capacity, harnessDefaults, holdings } from './capacity.mjs';
|
|
@@ -188,7 +188,22 @@ async function subscriberAlive(fleet, address) {
|
|
|
188
188
|
const session = sessionAt(address);
|
|
189
189
|
if (!session) return false;
|
|
190
190
|
await readBefore(fleet, session);
|
|
191
|
-
|
|
191
|
+
const alive = sessionAlive(fleet, session);
|
|
192
|
+
if (alive == null && subscriberGone(fleet, address)) return false;
|
|
193
|
+
return alive ?? null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A subscriber that is gone: no transcript, no process and no pane. Its machine's discovery answered whole (no source
|
|
197
|
+
* failed, nothing truncated) without the session, the fleet lists no pane for it, and the reading came from a daemon
|
|
198
|
+
* whose index names a Claude subagent by its own file (#1423, marked `subagent_rows: 'own'` on its discovery): an older
|
|
199
|
+
* daemon's index dropped a parent session that ran a subagent, so its absence there is never read as gone.
|
|
200
|
+
*/
|
|
201
|
+
function subscriberGone(fleet, address) {
|
|
202
|
+
if (fleet.alive.has(address) || fleet.native?.has(address)) return false;
|
|
203
|
+
const session = sessionAt(address);
|
|
204
|
+
const seen = session && discoveries.get(nativeKey(session.machine, session.harness, session.session_id));
|
|
205
|
+
if (!seen?.complete) return false;
|
|
206
|
+
return !seen.sessions.some((r) => r.locator?.session_id === session.session_id && r.locator?.harness === session.harness);
|
|
192
207
|
}
|
|
193
208
|
const paneLists = new Map();
|
|
194
209
|
/** A session the fleet reading did not publish: alive by what was read of it before (readBefore), else unknown (null). */
|
|
@@ -242,8 +257,8 @@ function nativeSessions(machine, harness, sessionId) {
|
|
|
242
257
|
async function readNative(machine, harness, sessionId) {
|
|
243
258
|
const key = nativeKey(machine, harness, sessionId), seen = discoveries.get(key);
|
|
244
259
|
if (seen) return seen.sessions;
|
|
245
|
-
const sessions = await discoverNative(machine, harness, sessionId);
|
|
246
|
-
discoveries.set(key, { at: Date.now(), sessions });
|
|
260
|
+
const { sessions, complete } = await discoverNative(machine, harness, sessionId);
|
|
261
|
+
discoveries.set(key, { at: Date.now(), sessions, complete });
|
|
247
262
|
return sessions;
|
|
248
263
|
}
|
|
249
264
|
// A native discovery is bounded as the fleet reading is: one that does not answer in time is unknown, read next round.
|
|
@@ -255,7 +270,9 @@ async function discoverNative(machine, harness, sessionId) {
|
|
|
255
270
|
const result = await doorOn(machine, 'harness.v1.sessions.discover', { harnesses: [harness], query: sessionId, limit: 2000 }, { timeout: NATIVE_DISCOVERY_MS });
|
|
256
271
|
const page = result.code === 0 ? lastJson(result.stdout) : null;
|
|
257
272
|
if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
|
|
258
|
-
|
|
273
|
+
// whole: every source read, nothing truncated, from an index that keeps every session's own row (subscriberGone)
|
|
274
|
+
const complete = page.subagent_rows === 'own' && !(Array.isArray(page.failed) && page.failed.length) && !page.receipt?.truncated && page.next_cursor == null;
|
|
275
|
+
return { sessions: page.sessions, complete };
|
|
259
276
|
}
|
|
260
277
|
function nativeReading(rows, session) {
|
|
261
278
|
const row = rows.find(r => r.locator?.session_id === session.session_id && r.locator?.harness === session.harness);
|
|
@@ -792,6 +809,9 @@ async function startCards(board, fleet, lanes, settings, report, entry, effects,
|
|
|
792
809
|
} catch (error) {
|
|
793
810
|
// a launch whose run ended meanwhile is no failure of the card's current run (its successor): said, nothing fired
|
|
794
811
|
if (error.launchPending || error.launchSuperseded) { report.queued.push([task.id, error.message]); continue; }
|
|
812
|
+
// the board's store could not be written (a full or failing volume): no fault of the card, which keeps its failure
|
|
813
|
+
// count and is launched again next round (its launch key reconciles a session the refused write did not record)
|
|
814
|
+
if (storageFault(error)) { report.queued.push([task.id, `the board's store refused a write (${error.message}); the launch is tried again next round`]); continue; }
|
|
795
815
|
const payload = { error: String(error.message).slice(-500), origin: originOf(error), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
|
|
796
816
|
let r;
|
|
797
817
|
try { r = launchFailed(board, entry, task.id, error, payload); } catch { r = { effects: [] }; /* no transition for it */ }
|
|
@@ -966,7 +986,10 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
|
|
|
966
986
|
// The programs a harness pane runs in its foreground: Claude Code's, Codex's, and node (the daemon's Codex pane host,
|
|
967
987
|
// a harness installed through npm). A pane running one of these runs a harness session, read or not.
|
|
968
988
|
const HARNESS_COMMANDS = new Set(['claude', 'claude.exe', 'codex', 'codex.exe', 'node']);
|
|
969
|
-
|
|
989
|
+
/** A board launch never bound to a session (no session id read for it) whose launch window has passed. */
|
|
990
|
+
export const unboundLaunch = (session, launchWindow, at = now()) => Boolean(session?.launch_key && !session.session_id && launchWindow
|
|
991
|
+
&& Number(session.launched_at) > 0 && at - Number(session.launched_at) >= launchWindow);
|
|
992
|
+
async function turnAtClose(session, pane = null, { launchWindow = null } = {}) {
|
|
970
993
|
const machine = session.machine ?? localMachine();
|
|
971
994
|
let activity;
|
|
972
995
|
if (pane?.pid) {
|
|
@@ -977,6 +1000,10 @@ async function turnAtClose(session, pane = null) {
|
|
|
977
1000
|
// No session read under the pane is no absence (a registry it could not read, another config home, a failed process
|
|
978
1001
|
// read): the pane's own foreground program says whether a harness runs there. Only a pane running none is closed.
|
|
979
1002
|
if (!activity) {
|
|
1003
|
+
// A pane the board launched whose session no reading ever found (the fleet's never bound it, and none runs under
|
|
1004
|
+
// its process now) past the launch window holds a harness that never started a session: nothing is cut by its
|
|
1005
|
+
// close. Waiting on it kept t_7ec85b6b's Codex pane on aarons-mac-mini, stuck before its first turn, open for 15 h.
|
|
1006
|
+
if (unboundLaunch(session, launchWindow)) return null;
|
|
980
1007
|
const command = String(pane.command ?? '').split(/[\\/]/).pop().toLowerCase();
|
|
981
1008
|
return command && !HARNESS_COMMANDS.has(command) ? null : `pane ${session.pane} runs ${command || 'a program it does not name'}, whose session could not be read`;
|
|
982
1009
|
}
|
|
@@ -984,7 +1011,7 @@ async function turnAtClose(session, pane = null) {
|
|
|
984
1011
|
return `pane ${session.pane} names no process to read`;
|
|
985
1012
|
} else {
|
|
986
1013
|
if (!session.session_id) return 'its session is not known yet';
|
|
987
|
-
try { activity = nativeReading(await discoverNative(machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity ?? null; }
|
|
1014
|
+
try { activity = nativeReading((await discoverNative(machine, session.harness ?? 'claude-code', session.session_id)).sessions, session).row?.activity ?? null; }
|
|
988
1015
|
catch (error) { return `its session could not be read just now (${String(error.message).slice(0, 160)})`; }
|
|
989
1016
|
if (!activity) return `its session ${addressOf(session)} is not in its machine's discovery`;
|
|
990
1017
|
}
|
|
@@ -1108,7 +1135,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
|
|
|
1108
1135
|
if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
|
|
1109
1136
|
// An ended run's session is closed only between its turns, read last, after the claim and the pane list (a pane
|
|
1110
1137
|
// already gone is settled above with no reading; t_f34710bc's coder was cut mid-proof at 15:01:13Z, 2026-10-04).
|
|
1111
|
-
if (row.ended_at) { const inTurn = await turnAtClose(session, observed.live.get(session.pane)); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
|
|
1138
|
+
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}`); }
|
|
1112
1139
|
await stopSession(board, session); observed.live.delete(session.pane);
|
|
1113
1140
|
}
|
|
1114
1141
|
}
|
|
@@ -2419,8 +2446,11 @@ function sessionRecord(db) {
|
|
|
2419
2446
|
*/
|
|
2420
2447
|
async function endWorkerSubscriptions(board, fleet, report) {
|
|
2421
2448
|
// each worker subscriber's liveness is read before the write
|
|
2422
|
-
const aliveNow = new Map();
|
|
2423
|
-
if (fleet.ok) for (const { chat_id: address } of board.db.prepare("SELECT DISTINCT chat_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all())
|
|
2449
|
+
const aliveNow = new Map(), goneNow = new Map();
|
|
2450
|
+
if (fleet.ok) for (const { chat_id: address } of board.db.prepare("SELECT DISTINCT chat_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all()) {
|
|
2451
|
+
aliveNow.set(address, await subscriberAlive(fleet, address));
|
|
2452
|
+
if (aliveNow.get(address) === false && subscriberGone(fleet, address)) goneNow.set(address, now());
|
|
2453
|
+
}
|
|
2424
2454
|
tx(board, (db) => {
|
|
2425
2455
|
const { held, latest } = sessionRecord(db);
|
|
2426
2456
|
const remove = db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?");
|
|
@@ -2429,7 +2459,9 @@ async function endWorkerSubscriptions(board, fleet, report) {
|
|
|
2429
2459
|
const address = row.chat_id, record = latest.get(address);
|
|
2430
2460
|
// a session alive, or whose liveness could not be read (unknown, never taken as ended), keeps its subscription
|
|
2431
2461
|
if (!record || record.session.released || held.has(address) || (fleet.ok && aliveNow.get(address) !== false)) continue;
|
|
2432
|
-
|
|
2462
|
+
// the board closed it, or it is gone (no transcript, process or pane, read whole: subscriberGone)
|
|
2463
|
+
const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at }
|
|
2464
|
+
: goneNow.has(address) ? { by: 'gone', run: record.run, read_at: goneNow.get(address), machine: sessionAt(address)?.machine ?? null } : null;
|
|
2433
2465
|
if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
|
|
2434
2466
|
if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
|
|
2435
2467
|
ended.get(address).cards.push(row.task_id);
|
package/board/store.mjs
CHANGED
|
@@ -329,6 +329,14 @@ export function publishMarked(root) {
|
|
|
329
329
|
export const contention = (error) => [5, 6].includes(Number(error?.errcode) & 255)
|
|
330
330
|
|| ['SQLITE_BUSY', 'SQLITE_LOCKED'].includes(error?.code)
|
|
331
331
|
|| /^database (?:table |schema )?is locked$/i.test(error?.message ?? '');
|
|
332
|
+
// The store's volume refusing a write (SQLite IOERR or FULL and their extended codes; ENOSPC or EIO from a file write):
|
|
333
|
+
// a fault of the machine's storage at that minute, never of the card or the operation that met it. A transaction it
|
|
334
|
+
// rolled back is tried again a bounded number of times (STORAGE_TRIES); one still refused fails cleanly, and its caller
|
|
335
|
+
// leaves the work for the next round instead of counting it against a card.
|
|
336
|
+
export const storageFault = (error) => [10, 13].includes(Number(error?.errcode) & 255)
|
|
337
|
+
|| ['SQLITE_IOERR', 'SQLITE_FULL', 'ENOSPC', 'EIO'].includes(error?.code)
|
|
338
|
+
|| /^(?:disk I\/O error|database or disk is full)$/i.test(error?.message ?? '');
|
|
339
|
+
export const STORAGE_TRIES = 3;
|
|
332
340
|
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
333
341
|
/** Run `fn` until it answers without meeting another process's write (a short, randomized wait between tries). */
|
|
334
342
|
export function retryContention(fn) {
|
|
@@ -349,7 +357,7 @@ export const view = (board, fn) => inTx.has(board.db) ? fn(board.db) : retryCont
|
|
|
349
357
|
const inTx = new WeakSet();
|
|
350
358
|
function sqliteTx(db, fn) {
|
|
351
359
|
if (inTx.has(db)) return fn(db);
|
|
352
|
-
for (
|
|
360
|
+
for (let faults = 0; ;) {
|
|
353
361
|
try {
|
|
354
362
|
db.exec('BEGIN');
|
|
355
363
|
inTx.add(db);
|
|
@@ -359,6 +367,7 @@ function sqliteTx(db, fn) {
|
|
|
359
367
|
return out;
|
|
360
368
|
} catch(error) {
|
|
361
369
|
try {db.exec('ROLLBACK');} catch {}
|
|
370
|
+
if (storageFault(error) && ++faults < STORAGE_TRIES) { Atomics.wait(pause, 0, 0, 50 * faults); continue; }
|
|
362
371
|
if (!contention(error))throw error;
|
|
363
372
|
Atomics.wait(pause, 0, 0, 1 + Math.random() * 20);
|
|
364
373
|
} finally { inTx.delete(db); }
|
package/board/ztrack.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
14
14
|
import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
16
|
import { DatabaseSync } from 'node:sqlite';
|
|
17
|
-
import { actorOf, contention, event, json, now, principalOf } from './store.mjs';
|
|
17
|
+
import { actorOf, contention, event, json, now, principalOf, storageFault, STORAGE_TRIES } from './store.mjs';
|
|
18
18
|
import { GitDocument } from './git-document.mjs';
|
|
19
19
|
import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
|
|
20
20
|
import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
@@ -418,7 +418,7 @@ class ZtrackBoard {
|
|
|
418
418
|
// A pull request merged on the remote (a page edited in Git Notes) is in the document before this write reads it.
|
|
419
419
|
if (this.git) this.git.paths = this.docs.map((doc) => doc.path);
|
|
420
420
|
try { timedStep('board git pull', () => this.git?.pull()); } catch (error) { current().err(`board: ${error.message}\n`); }
|
|
421
|
-
let txStarted = 0;
|
|
421
|
+
let txStarted = 0, faults = 0;
|
|
422
422
|
for (let attempt = 0; ; attempt++) {
|
|
423
423
|
let outcome;
|
|
424
424
|
try {
|
|
@@ -458,6 +458,8 @@ class ZtrackBoard {
|
|
|
458
458
|
try { this.conn.exec('ROLLBACK'); } catch { /* not begun */ }
|
|
459
459
|
this.active = false; this.seen = null; this.loadErrors();
|
|
460
460
|
if (error === RETRY || sqliteContention(error)) { backoff(attempt); noteStep('board lock wait', performance.now() - txStarted); continue; }
|
|
461
|
+
// the store's volume refused the write (store.mjs storageFault): rolled back, so tried again a bounded number of times
|
|
462
|
+
if (storageFault(error) && ++faults < STORAGE_TRIES) { sleep(50 * faults); continue; }
|
|
461
463
|
throw error;
|
|
462
464
|
} finally { this.active = false; }
|
|
463
465
|
this.seed = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.120",
|
|
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": {
|