@volter/supercode-orchestrator 0.4.126 → 0.4.128
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/cli.mjs +13 -1
- package/board/dispatch.mjs +66 -11
- package/board/engine.mjs +1 -0
- package/board/workflow.mjs +1 -0
- package/board/ztrack.mjs +12 -4
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -54,6 +54,8 @@ supercode's own:
|
|
|
54
54
|
notify-subscribe <id> --address sc:<machine>:<harness>:<id> [--role creator|worker|dependency]
|
|
55
55
|
Any verb … --at sc:<machine>:board:<home>: sent as mail to a board kept on another machine.
|
|
56
56
|
<event> <id>... [reason] [--<field> value]: any other event the board's workflow declares, its flags the event's fields.
|
|
57
|
+
start <id> release a --no-start hold and start through the declared workflow
|
|
58
|
+
create ... --adopt <session> hold dispatch until that live session is attached
|
|
57
59
|
goto <id> <status> [reason] [--<field> value]: the event the workflow says takes the card to that status, sent.
|
|
58
60
|
|
|
59
61
|
specify/decompose with no spec start a session on the triage card to write it. A card's own session may comment on,
|
|
@@ -294,11 +296,15 @@ export async function runWorkflow(argv) {
|
|
|
294
296
|
workspace, branch: args.branch, parents: args.parent, priority: args.priority, tenant: args.tenant,
|
|
295
297
|
triage: args.triage, idempotencyKey: args['idempotency-key'], maxRuntime: duration(args['max-runtime']),
|
|
296
298
|
maxRetries, skills: args.skill, model: args.model, provider: args.provider, createdBy: args['created-by'] ?? callerName(),
|
|
299
|
+
noStart: Boolean(args['no-start']), adopt: args.adopt ?? null,
|
|
297
300
|
every: args.every ?? null, sessionId: callerSession(), subtaskOf: args['subtask-of'] ?? null, initialStatus: args['initial-status'] ?? null,
|
|
298
301
|
project: args.project ?? meta.project_id ?? null, goal: args.goal ? { maxTurns: args['goal-max-turns'] ? Number(args['goal-max-turns']) : null } : args['no-goal'] ? false : null,
|
|
299
302
|
}), true);
|
|
300
303
|
let answer = { id: created.id, existing: created.existing };
|
|
301
|
-
if (
|
|
304
|
+
if (args.adopt) {
|
|
305
|
+
const adopted = withBoard(b => adoptSession(b, created.id, args.adopt, { ttl: settings.ttl }));
|
|
306
|
+
answer = { ...answer, started: true, session: adopted.address, mode: adopted.mode };
|
|
307
|
+
} else if (!args['no-start'] && !created.existing && !args['subtask-of']) {
|
|
302
308
|
const [report] = await tick({ root, lanes, settings, only: created.id, slugs: [slug] });
|
|
303
309
|
const started = report.started.find(([t]) => t === created.id);
|
|
304
310
|
const queued = report.queued.find(([t]) => t === created.id);
|
|
@@ -813,6 +819,12 @@ export async function runWorkflow(argv) {
|
|
|
813
819
|
say(args, lifted.length ? `Resumed (removed ${lifted.join(', ')})` : 'Not paused', { ok: true, lifted });
|
|
814
820
|
return 0;
|
|
815
821
|
}
|
|
822
|
+
case 'start': {
|
|
823
|
+
need(1, 'a card id');
|
|
824
|
+
const { lanes, settings } = await laneState(root);
|
|
825
|
+
const session = await withBoardAsync(b => startCard(b, id, { lanes, settings, roles: rolesFor(b.db, id, sid, workflowFor(root).params) }));
|
|
826
|
+
say(args, `${id}: ${session?.address ?? 'started'}`, { ok: true, session }); return 0;
|
|
827
|
+
}
|
|
816
828
|
case 'adopt': {
|
|
817
829
|
need(2, 'a card id and a session');
|
|
818
830
|
const { settings } = await laneState(root);
|
package/board/dispatch.mjs
CHANGED
|
@@ -136,7 +136,20 @@ const HARNESSES = {
|
|
|
136
136
|
const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
|
|
137
137
|
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
138
138
|
|
|
139
|
-
const sessionAlive = (fleet, session) =>
|
|
139
|
+
const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : Boolean(session?.session_id) && fleet.alive.has(addressOf(session));
|
|
140
|
+
|
|
141
|
+
/** Native activity comes from the machine's harness discovery, not catalogue age. */
|
|
142
|
+
function nativeSessions(machine, harness) {
|
|
143
|
+
const result = sc(['discover', '--on', machine, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
|
|
144
|
+
const page = result.code === 0 ? lastJson(result.stdout) : null;
|
|
145
|
+
if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
|
|
146
|
+
return page.sessions;
|
|
147
|
+
}
|
|
148
|
+
function nativeReading(rows, session) {
|
|
149
|
+
const row = rows.find(r => r.locator?.session_id === session.session_id && r.locator?.harness === session.harness);
|
|
150
|
+
const presence = row?.activity?.presence;
|
|
151
|
+
return { row, alive: presence === 'running' ? true : presence === 'persisted' ? false : null };
|
|
152
|
+
}
|
|
140
153
|
|
|
141
154
|
/** Match a manual continuation by the same harness identity and an actually live machine pane. */
|
|
142
155
|
function continuationPane(fleet, session, panes) {
|
|
@@ -211,6 +224,17 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
211
224
|
// The catalog can lag a pane. Read the actual machines once, before any board mutation.
|
|
212
225
|
const observed = new Map();
|
|
213
226
|
const exits = new Map();
|
|
227
|
+
const native = new Map(); fleet.native = new Map();
|
|
228
|
+
for (const { id } of rows) {
|
|
229
|
+
const session = currentRun(board.db, id)?.metadata?.supercode;
|
|
230
|
+
if (!session?.session_id || session.pane || session.surface === 'headless') continue;
|
|
231
|
+
const key = `${session.machine}:${session.harness}`;
|
|
232
|
+
if (!native.has(key)) {
|
|
233
|
+
try { native.set(key, nativeSessions(session.machine, session.harness)); }
|
|
234
|
+
catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
|
|
235
|
+
}
|
|
236
|
+
fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
|
|
237
|
+
}
|
|
214
238
|
for (const { id } of rows) {
|
|
215
239
|
const s = currentRun(board.db, id)?.metadata?.supercode;
|
|
216
240
|
if (!s?.pane || observed.has(s.machine)) continue;
|
|
@@ -288,7 +312,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
288
312
|
// Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
|
|
289
313
|
const paneReading = session?.pane ? observed.get(session.machine) : undefined;
|
|
290
314
|
const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
|
|
291
|
-
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) : true) : false;
|
|
315
|
+
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
|
|
292
316
|
// (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
|
|
293
317
|
if ((!task.claim_lock || !task.claim_expires) && !live) {
|
|
294
318
|
observe('orphaned', { reason: 'orphaned_running', claim_lock: task.claim_lock, claim_expires: task.claim_expires, error: 'orphaned running card (broken claim bookkeeping)' }, 'reclaimed');
|
|
@@ -325,6 +349,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
325
349
|
if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
|
|
326
350
|
if (!session.pane && !fleet.ok) return;
|
|
327
351
|
const alive = session.pane ? paneLive : sessionAlive(fleet, session);
|
|
352
|
+
if (alive == null) return; // native discovery was inconclusive
|
|
328
353
|
const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
|
|
329
354
|
const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
|
|
330
355
|
const missing = !alive && machineUp && !young;
|
|
@@ -374,6 +399,13 @@ function stopSession(board, session) {
|
|
|
374
399
|
if (pidAlive(session.pid)) signal('SIGKILL');
|
|
375
400
|
return;
|
|
376
401
|
}
|
|
402
|
+
if (!session.pane && session.surface === 'native') {
|
|
403
|
+
const r = sc(['close', addressOf(session)], { timeout: 120_000, as: boardAddress(board.label) });
|
|
404
|
+
if (r.code !== 0) throw new Error(`closing native session ${addressOf(session)}: ${(r.stderr || r.stdout).trim().slice(-500)}`);
|
|
405
|
+
const reading = nativeReading(nativeSessions(session.machine, session.harness), session);
|
|
406
|
+
if (reading.alive !== false) throw new Error(`native close has no confirmed stopped receipt for ${addressOf(session)}`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
377
409
|
if (session.pane) {
|
|
378
410
|
const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
|
|
379
411
|
if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
|
|
@@ -421,6 +453,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
421
453
|
for (const task of everyCard) {
|
|
422
454
|
if (only && task.id !== only) continue;
|
|
423
455
|
const state = scopeFor(task).card;
|
|
456
|
+
if (state.dispatch_hold) continue;
|
|
424
457
|
if (workflow.run && (task.subtask_of || state.paused || state.archived || state.blocked_by.length || task.current_run_id || (state.every && state.next_run_at > now()))) continue;
|
|
425
458
|
if (!startable.has(statusOf(board.db, task))) continue;
|
|
426
459
|
const plan = planOf(task, scopeFor(task));
|
|
@@ -565,6 +598,8 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
|
|
|
565
598
|
const moved = tx(board, (db) => {
|
|
566
599
|
assertWorkspaceOwner(db, before, checked);
|
|
567
600
|
if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
|
|
601
|
+
if (!roles.includes('manager')) throw new Error('Only the manager starts a held card; request its decision.');
|
|
602
|
+
event(db, id, 'dispatch_released', { reason: 'explicit start' });
|
|
568
603
|
return fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } });
|
|
569
604
|
});
|
|
570
605
|
const task = getTaskNow(board, id);
|
|
@@ -611,7 +646,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
611
646
|
report.undelivered.push([row.task_id, addressOf(session), error]);
|
|
612
647
|
}
|
|
613
648
|
}
|
|
614
|
-
if (!session?.pane || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
649
|
+
if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
615
650
|
if (caller && session.session_id === caller) continue; // let the command that ended its own run return
|
|
616
651
|
const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
|
|
617
652
|
// Persist retries even for historical ends that did not have a close_session action.
|
|
@@ -619,6 +654,16 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
619
654
|
let error = null;
|
|
620
655
|
try {
|
|
621
656
|
const machine = session.machine ?? localMachine();
|
|
657
|
+
if (session.surface === 'native' && !session.pane) {
|
|
658
|
+
const reading = nativeReading(nativeSessions(machine, session.harness), session);
|
|
659
|
+
if (reading.alive == null) throw new Error(`close deferred: native liveness unknown for ${addressOf(session)}`);
|
|
660
|
+
if (reading.alive) {
|
|
661
|
+
const protectedNow = active.some(r => addressOf(json(r.metadata, {})?.supercode) === addressOf(session));
|
|
662
|
+
if (protectedNow) throw new Error('close deferred: native session is owned by an unended run');
|
|
663
|
+
if (session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${session.lifecycle_notice_pending} has not been delivered`);
|
|
664
|
+
stopSession(board, session);
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
622
667
|
if (!panes.has(machine)) {
|
|
623
668
|
const r = sc(['teams', 'panes', 'ls', ...onMachine(machine)], { timeout: 60_000, as: boardAddress(board.label) });
|
|
624
669
|
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
@@ -634,10 +679,11 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
634
679
|
if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
|
|
635
680
|
stopSession(board, session); observed.live.delete(session.pane);
|
|
636
681
|
}
|
|
682
|
+
}
|
|
637
683
|
} catch (e) { error = String(e.message).slice(-500); }
|
|
638
684
|
tx(board, (db) => {
|
|
639
685
|
const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
|
640
|
-
if (!current || current.pane !== session.pane) return;
|
|
686
|
+
if (!current || current.pane !== session.pane || current.session_id !== session.session_id) return;
|
|
641
687
|
const at = now();
|
|
642
688
|
if (error) {
|
|
643
689
|
setRunSession(db, row.id, { ...current, close_attempted_at: at, close_attempts: Number(current.close_attempts ?? 0) + 1, close_error: error });
|
|
@@ -668,8 +714,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
668
714
|
if (!(entry.workflow.statuses[status]?.on?.session_idle || entry.workflow.on?.session_idle || entry.workflow.run?.on?.session_idle)) return;
|
|
669
715
|
const runRow = currentRun(db, id);
|
|
670
716
|
const session = runRow?.metadata?.supercode;
|
|
671
|
-
if (!session?.session_id || session.surface
|
|
672
|
-
|
|
717
|
+
if (!session?.session_id || !['pane', 'native'].includes(session.surface) || session.offline_since != null) return;
|
|
718
|
+
if (session.surface === 'native' && fleet.native?.get(addressOf(session))?.alive !== true) return;
|
|
719
|
+
const live = fleet.sessions.find((x) => x.address === addressOf(session));
|
|
673
720
|
const observedAt = now();
|
|
674
721
|
const lastAt = Math.floor(Number(live?.last_message_at_ms) / 1000);
|
|
675
722
|
const reading = {
|
|
@@ -695,7 +742,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
695
742
|
try {
|
|
696
743
|
const where = whereOf(board, task);
|
|
697
744
|
// the ask names what the session may do, as its run's prompt did
|
|
698
|
-
const said = doors(entry.workflow, status, session.role ?? runSpec(entry, status)?.role ?? 'implementer', id, where, session.surface
|
|
745
|
+
const said = doors(entry.workflow, status, session.role ?? runSpec(entry, status)?.role ?? 'implementer', id, where, session.surface !== 'headless' ? 'pane' : 'headless');
|
|
699
746
|
const r = fire(db, board, entry, id, 'session_idle', { roles: ['platform'], payload: { ...reading, turns: reading.turns_before, worked }, extra: { where, doors: said } });
|
|
700
747
|
effects.push(...r.effects);
|
|
701
748
|
const after = currentRun(db, id);
|
|
@@ -948,7 +995,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
948
995
|
closeRequested(board, { undelivered: [] }, { taskId: task.id });
|
|
949
996
|
const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
|
|
950
997
|
.map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
|
|
951
|
-
.find((r) => (r.session?.pane || r.session?.launch_key) && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
|
|
998
|
+
.find((r) => (r.session?.pane || r.session?.launch_key || r.session?.surface === 'native') && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
|
|
952
999
|
if (pending) throw unavailable(`slot ${slot} still owns pane ${pending.session.pane} from run ${pending.id}; closure must be confirmed before another session starts: ${pending.session.close_error ?? 'close pending'}`);
|
|
953
1000
|
let prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
954
1001
|
if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
|
|
@@ -997,6 +1044,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
997
1044
|
};
|
|
998
1045
|
let session = null;
|
|
999
1046
|
let replaced = null;
|
|
1047
|
+
if (prev?.surface === 'native' && !prev.pane) {
|
|
1048
|
+
const reading = nativeReading(nativeSessions(prev.machine, prev.harness), prev);
|
|
1049
|
+
if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
|
|
1050
|
+
fleet.native ??= new Map(); fleet.native.set(addressOf(prev), reading);
|
|
1051
|
+
if (reading.alive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw unavailable(`native session ${addressOf(prev)} is still live; its close must finish before replacement`);
|
|
1052
|
+
}
|
|
1000
1053
|
if (prev?.forgotten) {
|
|
1001
1054
|
if (prev.pane && panePresent(prev.machine, prev.pane)) throw new Error(`forgotten session ${prev.session_id} still has live pane ${prev.pane}; close it before replacement`);
|
|
1002
1055
|
replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
|
|
@@ -1165,14 +1218,15 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
1165
1218
|
if (!found) throw new Error(`the fleet lists no live session ${who}`);
|
|
1166
1219
|
const [, host, harness, sessionId] = found.address.split(':');
|
|
1167
1220
|
if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
|
|
1168
|
-
|
|
1221
|
+
const native = found.pane ? null : nativeReading(nativeSessions(host, harness), { session_id: sessionId, harness });
|
|
1222
|
+
if (found.pane ? !panePresent(host, found.pane) : native.alive !== true) throw new Error(`${found.address} has no verified live native process or pane`);
|
|
1169
1223
|
const beforeRun = view(board, (db) => currentRun(db, id));
|
|
1170
1224
|
const beforeSession = beforeRun?.metadata?.supercode;
|
|
1171
1225
|
if (beforeRun && beforeSession?.session_id !== sessionId) throw new Error(`${id} has another active run/session; end it before adopting a different identity`);
|
|
1172
1226
|
if (beforeSession?.pane && beforeSession.pane !== found.pane && panePresent(beforeSession.machine, beforeSession.pane)) throw new Error(`${id} still has live pane ${beforeSession.pane}; close the extra pane before rebinding ${sessionId}`);
|
|
1173
1227
|
|
|
1174
1228
|
// the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
|
|
1175
|
-
let dir = workspace;
|
|
1229
|
+
let dir = workspace ?? native?.row?.cwd;
|
|
1176
1230
|
if (!dir) {
|
|
1177
1231
|
const r = sc(['discover', '--on', host, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
|
|
1178
1232
|
dir = (json(r.stdout.trim(), {})?.sessions ?? []).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
|
|
@@ -1203,10 +1257,11 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
1203
1257
|
const implementer = lastSession(db, id, 'worker');
|
|
1204
1258
|
if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
|
|
1205
1259
|
}
|
|
1260
|
+
event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
|
|
1206
1261
|
if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: ['manager'], payload: { lock: claimer() } });
|
|
1207
1262
|
const runId = getTask(db, id).current_run_id;
|
|
1208
1263
|
if (!runId) throw new Error(`${id} waits on parents that are not done`);
|
|
1209
|
-
const session = { surface: 'pane', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
1264
|
+
const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
1210
1265
|
setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: currentRun(db, id)?.metadata?.supercode?.slot ?? beforeSession?.slot ?? expected?.session ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
|
|
1211
1266
|
event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt', role: currentRun(db, id)?.metadata?.supercode?.role }, runId);
|
|
1212
1267
|
subscribeIn(db, id, found.address, 'worker');
|
package/board/engine.mjs
CHANGED
|
@@ -216,6 +216,7 @@ export function createCard(board, spec) {
|
|
|
216
216
|
const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
|
|
217
217
|
every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
|
|
218
218
|
event(db, id, 'created', { creator: actorOf(), assignee: spec.assignee || null, status, parents, tenant: spec.tenant ?? null, ...(machine ? { machine } : {}), ...(arc ? { subtask_of: arc.id } : {}) });
|
|
219
|
+
if ((spec.noStart || spec.adopt) && !arc) event(db, id, 'dispatch_held', { session: spec.adopt ?? null, reason: spec.adopt ? 'awaiting adoption' : 'created with --no-start' });
|
|
219
220
|
if (parents.length) inheritSubscriptions(db, id, parents);
|
|
220
221
|
if (arc) {
|
|
221
222
|
// the arc's subscribers hear its subtasks, and the arc's session works them
|
package/board/workflow.mjs
CHANGED
|
@@ -222,6 +222,7 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
222
222
|
const card = {
|
|
223
223
|
...task,
|
|
224
224
|
creator_address: creatorOf(db, task.id),
|
|
225
|
+
dispatch_hold: one(db, "SELECT kind FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','session_adopted') ORDER BY id DESC LIMIT 1", task.id)?.kind === 'dispatch_held',
|
|
225
226
|
status,
|
|
226
227
|
blocked_by: blocks,
|
|
227
228
|
paused: json(state.paused, null),
|
package/board/ztrack.mjs
CHANGED
|
@@ -19,6 +19,14 @@ const CARD_MAIN = new Set(['id', 'title', 'body', 'status', 'assignee', 'priorit
|
|
|
19
19
|
const EPHEMERAL = new Set(['tasks', 'supercode_cards', 'task_links', 'sqlite_sequence']);
|
|
20
20
|
const atomic = (path, text) => { const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, text, { mode: 0o600 }); renameSync(tmp, path); };
|
|
21
21
|
|
|
22
|
+
// Only SQLite's actual contention codes retry. Rule refusals can contain words
|
|
23
|
+
// such as "blocked" or "busy" and must reach the caller unchanged.
|
|
24
|
+
const sqliteContention = error => [5, 6].includes(Number(error?.errcode) & 255)
|
|
25
|
+
|| ['SQLITE_BUSY', 'SQLITE_LOCKED'].includes(error?.code)
|
|
26
|
+
|| /^database (?:table |schema )?is locked$/i.test(error?.message ?? '');
|
|
27
|
+
const exportConflict = error => error?.message === 'Hermes wrote during board export; retained its events for the next read';
|
|
28
|
+
const recoveryConflict = error => error?.message === 'document changed during recovery; retry';
|
|
29
|
+
|
|
22
30
|
export function openZtrackBoard(marker, { create = false, document = null, seed = null, init, root, slug, compatPath }) {
|
|
23
31
|
if (!existsSync(marker)) {
|
|
24
32
|
if (!create) throw new Error(`no ztrack board ${marker}`);
|
|
@@ -60,7 +68,7 @@ class ZtrackBoard {
|
|
|
60
68
|
const fileStamp = (path) => { try { const s = statSync(path); return `${s.ino}:${s.size}:${s.mtimeMs}`; } catch { return 'missing'; } };
|
|
61
69
|
return [fileStamp(this.path), fileStamp(this.archive), fileStamp(this.compatPath), this.get('version', '')].join('|');
|
|
62
70
|
}
|
|
63
|
-
readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(
|
|
71
|
+
readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(!sqliteContention(error))throw error; } } }
|
|
64
72
|
access(fn, reading = false) {
|
|
65
73
|
if (reading && !this.reading) { this.reading=true; try {return this.readOnly(()=>this.access(fn,true));}finally{this.reading=false;} }
|
|
66
74
|
if (this.active || (reading && this.mem && this.loadedStamp === this.stamp())) return fn();
|
|
@@ -112,7 +120,7 @@ class ZtrackBoard {
|
|
|
112
120
|
this.readOnly(()=>{ if(this.runtime.prepare('PRAGMA journal_mode').get().journal_mode!=='wal')this.runtime.exec('PRAGMA journal_mode=WAL'); });
|
|
113
121
|
for (;;) {
|
|
114
122
|
try { this.runtime.exec('BEGIN'); }
|
|
115
|
-
catch (error) { if (
|
|
123
|
+
catch (error) { if (!sqliteContention(error)) throw error; continue; }
|
|
116
124
|
this.active = true;
|
|
117
125
|
let committed = false;
|
|
118
126
|
try {
|
|
@@ -173,14 +181,14 @@ class ZtrackBoard {
|
|
|
173
181
|
this.put('export_pending', [...this.get('export_pending', []), ...changes]);
|
|
174
182
|
this.put('version', randomBytes(8).toString('hex'));
|
|
175
183
|
this.runtime.exec('COMMIT');
|
|
176
|
-
try { this.flushExport(); } catch (error) { if (
|
|
184
|
+
try { this.flushExport(); } catch (error) { if (!sqliteContention(error) && !exportConflict(error)) throw error; }
|
|
177
185
|
this.loadedStamp = this.stamp();
|
|
178
186
|
this.seed = null;
|
|
179
187
|
return result;
|
|
180
188
|
} catch (error) {
|
|
181
189
|
try { this.runtime.exec('ROLLBACK'); } catch { /* intent already durable */ }
|
|
182
190
|
this.invalidate();
|
|
183
|
-
if (
|
|
191
|
+
if (sqliteContention(error) || exportConflict(error) || recoveryConflict(error)) continue;
|
|
184
192
|
if (committed) error.message += ' (the operation reached its durable intent; inspect its recorded outcome before retrying)';
|
|
185
193
|
throw error;
|
|
186
194
|
} finally { this.active = false; this.computing = false; this.pendingValues = null; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.128",
|
|
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": {
|