@volter/supercode-orchestrator 0.5.119 → 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 */ }
@@ -991,7 +1011,7 @@ async function turnAtClose(session, pane = null, { launchWindow = null } = {}) {
991
1011
  return `pane ${session.pane} names no process to read`;
992
1012
  } else {
993
1013
  if (!session.session_id) return 'its session is not known yet';
994
- 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; }
995
1015
  catch (error) { return `its session could not be read just now (${String(error.message).slice(0, 160)})`; }
996
1016
  if (!activity) return `its session ${addressOf(session)} is not in its machine's discovery`;
997
1017
  }
@@ -2426,8 +2446,11 @@ function sessionRecord(db) {
2426
2446
  */
2427
2447
  async function endWorkerSubscriptions(board, fleet, report) {
2428
2448
  // each worker subscriber's liveness is read before the write
2429
- const aliveNow = new Map();
2430
- 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
+ }
2431
2454
  tx(board, (db) => {
2432
2455
  const { held, latest } = sessionRecord(db);
2433
2456
  const remove = db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?");
@@ -2436,7 +2459,9 @@ async function endWorkerSubscriptions(board, fleet, report) {
2436
2459
  const address = row.chat_id, record = latest.get(address);
2437
2460
  // a session alive, or whose liveness could not be read (unknown, never taken as ended), keeps its subscription
2438
2461
  if (!record || record.session.released || held.has(address) || (fleet.ok && aliveNow.get(address) !== false)) continue;
2439
- 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;
2440
2465
  if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
2441
2466
  if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
2442
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.119",
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": {