@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 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: here && checkoutPresent(resolved.path) };
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();
@@ -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
- return sessionAlive(fleet, session) ?? null;
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
- return page.sessions;
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
- async function turnAtClose(session, pane = null) {
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()) aliveNow.set(address, await subscriberAlive(fleet, address));
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
- const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at } : null;
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.118",
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": {