@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 +15 -2
- package/board/dispatch.mjs +34 -9
- 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 */ }
|
|
@@ -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())
|
|
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
|
-
|
|
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.
|
|
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": {
|