@volter/supercode-orchestrator 0.4.106 → 0.4.108
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/dispatch.mjs +44 -8
- package/board/engine.mjs +3 -1
- package/board/rows.mjs +1 -1
- package/board/ztrack.mjs +9 -3
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -63,6 +63,8 @@ function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const lastJson = (text) => {
|
|
66
|
+
const whole = json(text.trim(), null);
|
|
67
|
+
if (whole && typeof whole === 'object') return whole;
|
|
66
68
|
const line = text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('{') || l.startsWith('[')).pop();
|
|
67
69
|
return line ? json(line, null) : null;
|
|
68
70
|
};
|
|
@@ -120,6 +122,16 @@ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0,
|
|
|
120
122
|
|
|
121
123
|
const sessionAlive = (fleet, session) => Boolean(session?.session_id) && [...fleet.alive].some((a) => a.endsWith(`:${session.session_id}`));
|
|
122
124
|
|
|
125
|
+
/** Match a manual continuation by the same harness identity and an actually live machine pane. */
|
|
126
|
+
function continuationPane(fleet, session, panes) {
|
|
127
|
+
if (!session?.session_id || !(panes instanceof Set)) return null;
|
|
128
|
+
const candidates = new Set((fleet.sessions ?? []).filter((s) => s.address === addressOf(session) && s.pane && panes.has(s.pane)).map((s) => s.pane));
|
|
129
|
+
if (session.pane && panes.has(session.pane)) candidates.add(session.pane);
|
|
130
|
+
if (candidates.size > 1) throw new Error(`session ${session.session_id} has multiple live panes: ${[...candidates].join(', ')}; close the extra pane before continuing`);
|
|
131
|
+
return [...candidates][0] ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
123
135
|
function pidAlive(pid) {
|
|
124
136
|
if (!pid) return false;
|
|
125
137
|
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
@@ -191,7 +203,17 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
191
203
|
const task = getTask(db, id);
|
|
192
204
|
if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
|
|
193
205
|
const runRow = currentRun(db, id);
|
|
194
|
-
|
|
206
|
+
let session = runRow?.metadata?.supercode ?? null;
|
|
207
|
+
if (session?.pane) {
|
|
208
|
+
let found;
|
|
209
|
+
try { found = continuationPane(fleet, session, observed.get(session.machine)); }
|
|
210
|
+
catch (error) { report.failed.push([id, error.message]); return; }
|
|
211
|
+
if (found && found !== session.pane) {
|
|
212
|
+
session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
|
|
213
|
+
setRunSession(db, runRow.id, session);
|
|
214
|
+
event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
195
217
|
const t = now();
|
|
196
218
|
// what the dispatcher saw becomes a platform event; the card's workflow says what it does
|
|
197
219
|
const observe = (name, payload, bucket) => {
|
|
@@ -485,10 +507,6 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
485
507
|
if (!session?.pane || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
486
508
|
if (caller && session.session_id === caller) continue; // let the command that ended its own run return
|
|
487
509
|
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());
|
|
488
|
-
if (active.some((r) => {
|
|
489
|
-
const s = json(r.metadata, {})?.supercode;
|
|
490
|
-
return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
|
|
491
|
-
})) continue;
|
|
492
510
|
// Persist retries even for historical ends that did not have a close_session action.
|
|
493
511
|
tx(board, (db) => requestSessionClose(db, row.task_id, { runId: row.id }));
|
|
494
512
|
let error = null;
|
|
@@ -676,6 +694,16 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
676
694
|
.find((r) => r.session?.pane && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
|
|
677
695
|
if (pending) throw new Error(`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'}`);
|
|
678
696
|
let prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
697
|
+
if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
|
|
698
|
+
const fresh = observeFleet();
|
|
699
|
+
if (!fresh.ok) throw new Error(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
|
|
700
|
+
const r = sc(['teams', 'panes', 'ls', '--machine', machine], { timeout: 60_000 });
|
|
701
|
+
const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
702
|
+
if (!Array.isArray(listed)) throw new Error(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
703
|
+
const pane = continuationPane(fresh, prev, new Set(listed.map((p) => p.pane)));
|
|
704
|
+
if (pane && pane !== prev.pane) prev = { ...prev, pane, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
|
|
705
|
+
fleet = fresh;
|
|
706
|
+
}
|
|
679
707
|
if (prev?.close_requested && !prev.closed_at) {
|
|
680
708
|
const report = { undelivered: [] };
|
|
681
709
|
closeRequested(board, report, { taskId: task.id });
|
|
@@ -886,6 +914,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
886
914
|
if (!found) throw new Error(`the fleet lists no live session ${who}`);
|
|
887
915
|
const [, host, harness, sessionId] = found.address.split(':');
|
|
888
916
|
if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
|
|
917
|
+
if (!found.pane || !panePresent(host, found.pane)) throw new Error(`${found.address} has no verified live pane`);
|
|
918
|
+
const beforeRun = view(board, (db) => currentRun(db, id));
|
|
919
|
+
const beforeSession = beforeRun?.metadata?.supercode;
|
|
920
|
+
if (beforeRun && beforeSession?.session_id !== sessionId) throw new Error(`${id} has another active run/session; end it before adopting a different identity`);
|
|
921
|
+
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}`);
|
|
922
|
+
|
|
889
923
|
// the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
|
|
890
924
|
let dir = workspace;
|
|
891
925
|
if (!dir) {
|
|
@@ -899,7 +933,8 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
899
933
|
return tx(board, (db) => {
|
|
900
934
|
const task = getTask(db, id);
|
|
901
935
|
if (!task) throw new Error(`no card ${id}`);
|
|
902
|
-
if (task.
|
|
936
|
+
if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
|
|
937
|
+
if (beforeRun && assignee && assignee !== task.assignee) throw new Error('rebinding a session does not reassign its active run');
|
|
903
938
|
if (task.machine && normalMachine(task.machine) !== host) throw new Error(`${id} runs on ${task.machine}, and ${found.address} is on ${host}; move the card first`);
|
|
904
939
|
const was = lastSession(db, id);
|
|
905
940
|
if (was && was.session_id !== sessionId && sessionAlive(fleet, was)) throw new Error(`${id} already has a live session, ${was.address}`);
|
|
@@ -910,11 +945,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
910
945
|
db.prepare("UPDATE tasks SET workspace_kind = 'dir', workspace_path = ? WHERE id = ?").run(dir.replace(/^dir:/, ''), id);
|
|
911
946
|
}
|
|
912
947
|
// the workflow's adopt event: the card is claimed for its implementer, whose session this now is
|
|
913
|
-
|
|
948
|
+
const entry = workflowFor(board.root);
|
|
949
|
+
if (!beforeRun) fire(db, board, entry, id, entry.workflow.run ? entry.workflow.dispatch.start : 'adopt', { roles: ['manager'], payload: { lock: claimer() } });
|
|
914
950
|
const runId = getTask(db, id).current_run_id;
|
|
915
951
|
if (!runId) throw new Error(`${id} waits on parents that are not done`);
|
|
916
952
|
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() };
|
|
917
|
-
setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: 'worker' });
|
|
953
|
+
setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: beforeSession?.slot ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
|
|
918
954
|
event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt' }, runId);
|
|
919
955
|
subscribeIn(db, id, found.address, 'worker');
|
|
920
956
|
for (const p of db.prepare('SELECT parent_id FROM task_links WHERE child_id = ?').all(id)) subscribeIn(db, p.parent_id, found.address, 'dependency');
|
package/board/engine.mjs
CHANGED
|
@@ -204,7 +204,8 @@ export function createCard(board, spec) {
|
|
|
204
204
|
inheritSubscriptions(db, id, [arc.id], { workers: true });
|
|
205
205
|
event(db, arc.id, 'subtask_added', { subtask: id, title });
|
|
206
206
|
}
|
|
207
|
-
if (initial === 'running')
|
|
207
|
+
if (initial === 'running' && arc && entry.workflow.run) setStatus(db, entry.workflow, id, 'running');
|
|
208
|
+
else if (initial === 'running') claimIn(db, id, { ttl: claimTtl() });
|
|
208
209
|
return { id, existing: false };
|
|
209
210
|
});
|
|
210
211
|
}
|
|
@@ -247,6 +248,7 @@ export function claim(board, id, { ttl, lock = claimer(), expectStatus = 'ready'
|
|
|
247
248
|
|
|
248
249
|
/** `claim` inside the caller's own transaction (a dispatcher's guard and claim see one state). */
|
|
249
250
|
export function claimIn(db, id, { ttl = claimTtl(), lock = claimer(), expectStatus = null } = {}) {
|
|
251
|
+
if (getTask(db, id)?.subtask_of) throw new Error('a subtask is worked by its arc and has no run of its own');
|
|
250
252
|
// a triage card is claimed to be specified, which does not wait on its parents
|
|
251
253
|
if (expectStatus !== 'triage' && !parentsDone(db, id)) {
|
|
252
254
|
run(db, "UPDATE tasks SET status = 'todo' WHERE id = ? AND status = 'ready'", id);
|
package/board/rows.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// tables, run fields and small indexing metadata. No application locks or snapshot rewrites.
|
|
3
3
|
import { json } from './store.mjs';
|
|
4
4
|
const quote = s => `"${s.replaceAll('"', '""')}"`;
|
|
5
|
-
const RUNTIME = new Set(['claim_lock','claim_expires','worker_pid','last_heartbeat_at','current_run_id','consecutive_failures','last_failure_error','block_recurrences','session_id']);
|
|
5
|
+
const RUNTIME = new Set(['claim_lock','claim_expires','worker_pid','last_heartbeat_at','current_run_id','consecutive_failures','last_failure_error','block_recurrences','session_id','workspace_path']);
|
|
6
6
|
const OMIT = new Set(['tasks','supercode_cards','task_links','sqlite_sequence']);
|
|
7
7
|
export class RuntimeRows {
|
|
8
8
|
constructor(db) { this.db=db; this.revision=null; this.tables=new Map(); this.runtime={}; this.clocks={}; this.cachedTasks={}; this.cachedCards={}; }
|
package/board/ztrack.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const hash = (text) => createHash('sha256').update(text).digest('hex');
|
|
|
14
14
|
const read = (path) => existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
15
15
|
const quote = (s) => `"${s.replaceAll('"', '""')}"`;
|
|
16
16
|
const nativeId = (id) => /^t-[a-f0-9]+$/.test(id) ? id.replace('t-', 't_') : id;
|
|
17
|
-
const RUNTIME = new Set(['claim_lock', 'claim_expires', 'worker_pid', 'last_heartbeat_at', 'current_run_id', 'consecutive_failures', 'last_failure_error', 'block_recurrences', 'session_id']);
|
|
17
|
+
const RUNTIME = new Set(['claim_lock', 'claim_expires', 'worker_pid', 'last_heartbeat_at', 'current_run_id', 'consecutive_failures', 'last_failure_error', 'block_recurrences', 'session_id', 'workspace_path']);
|
|
18
18
|
const CARD_MAIN = new Set(['id', 'title', 'body', 'status', 'assignee', 'priority', 'workspace_kind', 'workspace_path', 'branch_name']);
|
|
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); };
|
|
@@ -370,7 +370,7 @@ class ZtrackBoard {
|
|
|
370
370
|
const workspace = /^(scratch|dir|worktree)(?::(.*))?$/.exec(card.workspace ?? 'scratch');
|
|
371
371
|
this.insert('tasks', { ...meta, ...this.saved.runtime?.[id], id, title: card.title, body: stored.body,
|
|
372
372
|
status: card.status, assignee: card.assignee ?? null, created_at: meta.created_at ?? now(), priority: card.priority ?? 0,
|
|
373
|
-
workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[2] ?? null, branch_name: card.branch ?? null });
|
|
373
|
+
workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[1] === 'scratch' ? (this.saved.runtime?.[id]?.workspace_path ?? meta.workspace_path ?? null) : workspace?.[2] ?? null, branch_name: card.branch ?? null });
|
|
374
374
|
this.insert('supercode_cards', { ...this.saved.clocks?.[id], task_id: id, stage: card.status, machine: card.machine ?? null, subtask_of: parent,
|
|
375
375
|
blocked_by: JSON.stringify((card.relations ?? []).map((r) => ({ ...(meta.blocked_by ?? []).find((b) => b.id === nativeId(r.issueId)), id: nativeId(r.issueId), kind: /^[ma]-/.test(r.issueId) ? 'message' : 'card' }))),
|
|
376
376
|
paused: card.paused ? JSON.stringify(card.paused) : null, archived: meta.archived ? 1 : 0, every: card.every ?? null });
|
|
@@ -419,7 +419,7 @@ class ZtrackBoard {
|
|
|
419
419
|
const metadata = Object.fromEntries(Object.entries(row).filter(([k, v]) => !RUNTIME.has(k) && !CARD_MAIN.has(k) && v != null));
|
|
420
420
|
if (c.archived) metadata.archived = true;
|
|
421
421
|
if (json(c.blocked_by, []).length) metadata.blocked_by = json(c.blocked_by, []);
|
|
422
|
-
cards.set(row.id, { ...parsed, metadata, priority: row.priority ?? 0, workspace: row.workspace_kind + (row.workspace_path ? `:${row.workspace_path}` : ''),
|
|
422
|
+
cards.set(row.id, { ...parsed, metadata, priority: row.priority ?? 0, workspace: row.workspace_kind + (row.workspace_kind !== 'scratch' && row.workspace_path ? `:${row.workspace_path}` : ''),
|
|
423
423
|
branch: row.branch_name || undefined, machine: c.machine || undefined, paused: json(c.paused, null) ?? undefined, every: c.every || undefined,
|
|
424
424
|
relations: json(c.blocked_by, []).map((b) => ({ type: 'blocked-by', issueId: this.fileIds.get(b.id) ?? b.id })), acceptanceCriteria: [] });
|
|
425
425
|
}
|
|
@@ -528,12 +528,18 @@ class ZtrackBoard {
|
|
|
528
528
|
if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_export_events'").get()) {
|
|
529
529
|
compat.exec(`CREATE TABLE supercode_export_events (source_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL UNIQUE)`);
|
|
530
530
|
compat.exec(`INSERT INTO supercode_export_events SELECT id, id FROM task_events WHERE json_extract(payload, '$.__supercode_export') = 1`);
|
|
531
|
+
// Repair existing lowered holds on upgrade, once; subsequent exports stay proportional to changes.
|
|
532
|
+
for (const state of this.mem.prepare('SELECT task_id, paused, blocked_by FROM supercode_cards').all()) {
|
|
533
|
+
if (!json(state.paused, null) && !json(state.blocked_by, []).some((b) => b.kind === 'message')) continue;
|
|
534
|
+
projected.add(state.task_id); projectStatus(this.mem, this.board(), entry, state.task_id);
|
|
535
|
+
}
|
|
531
536
|
}
|
|
532
537
|
// Every import is event-based. Existing unconsumed acts must never be erased by an export race.
|
|
533
538
|
const cursor = this.get('hermes_cursor', 0);
|
|
534
539
|
const unseen = compat.prepare('SELECT payload FROM task_events WHERE id > ?').all(cursor).some((r) => !json(r.payload, {})?.__supercode_export);
|
|
535
540
|
if (unseen) throw new Error('Hermes wrote during board export; retained its events for the next read');
|
|
536
541
|
const sources = new Map(changes.map(c => [`${c.source.name}:${c.source.key}`, c.source]));
|
|
542
|
+
for (const id of projected) sources.set(`tasks:${JSON.stringify([id])}`, { name: 'tasks', key: JSON.stringify([id]), row: this.mem.prepare('SELECT * FROM tasks WHERE id = ?').get(id) });
|
|
537
543
|
for (const source of [...sources.values()]) {
|
|
538
544
|
const id = source.name === 'tasks' ? source.row?.id : source.name === 'supercode_cards' ? source.row?.task_id : null;
|
|
539
545
|
if (id) sources.set(`tasks:${JSON.stringify([id])}`, {name:'tasks',key:JSON.stringify([id]),row:this.mem.prepare('SELECT * FROM tasks WHERE id=?').get(id)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.108",
|
|
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": {
|