@volter/supercode-orchestrator 0.4.104 → 0.4.106
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 +28 -10
- package/board/engine.mjs +3 -3
- package/board/workflow.mjs +9 -0
- package/board/ztrack.mjs +42 -1
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -176,6 +176,16 @@ export function lanesOf(profiles = {}) {
|
|
|
176
176
|
|
|
177
177
|
function supervise(board, fleet, settings, report, entry, effects) {
|
|
178
178
|
const rows = board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all();
|
|
179
|
+
// The catalog can lag a pane. Read the actual machines once, before any board mutation.
|
|
180
|
+
const observed = new Map();
|
|
181
|
+
for (const { id } of rows) {
|
|
182
|
+
const s = currentRun(board.db, id)?.metadata?.supercode;
|
|
183
|
+
if (!s?.pane || observed.has(s.machine)) continue;
|
|
184
|
+
const r = sc(['teams', 'panes', 'ls', '--machine', s.machine ?? localMachine()], { timeout: 60_000 });
|
|
185
|
+
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
186
|
+
observed.set(s.machine, Array.isArray(list) ? new Set(list.map((p) => p.pane)) : null);
|
|
187
|
+
if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
|
|
188
|
+
}
|
|
179
189
|
for (const { id } of rows) {
|
|
180
190
|
tx(board, (db) => {
|
|
181
191
|
const task = getTask(db, id);
|
|
@@ -197,7 +207,9 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
197
207
|
};
|
|
198
208
|
const limits = limitsOf(db, board, entry, task, session);
|
|
199
209
|
// Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
|
|
200
|
-
const
|
|
210
|
+
const paneReading = session?.pane ? observed.get(session.machine) : undefined;
|
|
211
|
+
const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
|
|
212
|
+
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) : true) : false;
|
|
201
213
|
// (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
|
|
202
214
|
if ((!task.claim_lock || !task.claim_expires) && !live) {
|
|
203
215
|
observe('orphaned', { reason: 'orphaned_running', claim_lock: task.claim_lock, claim_expires: task.claim_expires, error: 'orphaned running card (broken claim bookkeeping)' }, 'reclaimed');
|
|
@@ -226,20 +238,21 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
226
238
|
return;
|
|
227
239
|
}
|
|
228
240
|
if (session?.session_id) {
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
const
|
|
241
|
+
if (session.pane && paneLive === null) return; // no machine reading, no loss verdict
|
|
242
|
+
if (!session.pane && !fleet.ok) return;
|
|
243
|
+
const alive = session.pane ? paneLive : sessionAlive(fleet, session);
|
|
244
|
+
const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
|
|
232
245
|
const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
|
|
233
246
|
const missing = !alive && machineUp && !young;
|
|
234
247
|
const next = { ...session };
|
|
235
248
|
// a machine that is off the fleet: how long, for the workflow's machine_offline (the claim is kept meanwhile)
|
|
236
|
-
if (machineUp)
|
|
249
|
+
if (machineUp) next.offline_since = null; else next.offline_since ??= t;
|
|
237
250
|
if (!machineUp) {
|
|
238
251
|
setRunSession(db, runRow.id, next);
|
|
239
252
|
observe('machine_offline', { machine: session.machine, seconds: t - next.offline_since }, null);
|
|
240
253
|
if (getTask(db, id)?.current_run_id !== runRow.id) return; // the workflow moved it
|
|
241
254
|
}
|
|
242
|
-
if (alive) { next.seen_at ??= t;
|
|
255
|
+
if (alive) { next.seen_at ??= t; next.missing_since = null; }
|
|
243
256
|
if (missing) next.missing_since ??= t;
|
|
244
257
|
if (missing && t - next.missing_since >= Number(limits.grace ?? settings.grace)) {
|
|
245
258
|
// lost: gone from an online machine for the whole grace. A resume that never came up is not resumable.
|
|
@@ -286,7 +299,7 @@ function stopSession(board, session) {
|
|
|
286
299
|
}
|
|
287
300
|
if (session.pane) {
|
|
288
301
|
const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
|
|
289
|
-
if (r.code !== 0 ||
|
|
302
|
+
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)}`);
|
|
290
303
|
}
|
|
291
304
|
}
|
|
292
305
|
|
|
@@ -471,10 +484,10 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
471
484
|
const session = json(row.metadata, {})?.supercode;
|
|
472
485
|
if (!session?.pane || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
473
486
|
if (caller && session.session_id === caller) continue; // let the command that ended its own run return
|
|
474
|
-
const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM
|
|
487
|
+
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());
|
|
475
488
|
if (active.some((r) => {
|
|
476
489
|
const s = json(r.metadata, {})?.supercode;
|
|
477
|
-
return
|
|
490
|
+
return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
|
|
478
491
|
})) continue;
|
|
479
492
|
// Persist retries even for historical ends that did not have a close_session action.
|
|
480
493
|
tx(board, (db) => requestSessionClose(db, row.task_id, { runId: row.id }));
|
|
@@ -488,7 +501,12 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
488
501
|
}
|
|
489
502
|
const observed = panes.get(machine);
|
|
490
503
|
if (observed.error) throw new Error(observed.error);
|
|
491
|
-
if (observed.live.has(session.pane)) {
|
|
504
|
+
if (observed.live.has(session.pane)) {
|
|
505
|
+
const protectedNow = view(board, (db) => db.prepare(`SELECT 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())
|
|
506
|
+
.some((r) => { const s = json(r.metadata, {})?.supercode; return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine); });
|
|
507
|
+
if (protectedNow) continue;
|
|
508
|
+
stopSession(board, session); observed.live.delete(session.pane);
|
|
509
|
+
}
|
|
492
510
|
} catch (e) { error = String(e.message).slice(-500); }
|
|
493
511
|
tx(board, (db) => {
|
|
494
512
|
const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
package/board/engine.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync, rmSync } from 'node:fs';
|
|
|
12
12
|
import { join, resolve } from 'node:path';
|
|
13
13
|
import { DatabaseSync } from 'node:sqlite';
|
|
14
14
|
import { view, claimer, event, json, newTaskId, now, tx } from './store.mjs';
|
|
15
|
-
import { refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
15
|
+
import { addDependency, refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
16
16
|
|
|
17
17
|
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
|
|
18
18
|
export const WORKSPACE_KINDS = ['scratch', 'dir', 'worktree'];
|
|
@@ -191,9 +191,9 @@ export function createCard(board, spec) {
|
|
|
191
191
|
spec.reasoning ?? null, spec.sessionId ?? null, spec.project ?? null, spec.goal ? 1 : 0, spec.goal?.maxTurns ?? null);
|
|
192
192
|
const machine = spec.machine ?? arc?.machine ?? null;
|
|
193
193
|
if (machine || arc) run(db, 'INSERT OR REPLACE INTO supercode_cards (task_id, machine, subtask_of) VALUES (?, ?, ?)', id, machine, arc?.id ?? null);
|
|
194
|
-
for (const p of parents) run(db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', p, id);
|
|
195
|
-
// where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
|
|
196
194
|
const entry = workflowFor(board.root);
|
|
195
|
+
for (const p of parents) addDependency(db, entry, id, p);
|
|
196
|
+
// where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
|
|
197
197
|
// the create's own words: whether it asked to be a goal card (null: the workflow decides), triage, its arc
|
|
198
198
|
const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
|
|
199
199
|
every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
|
package/board/workflow.mjs
CHANGED
|
@@ -521,6 +521,9 @@ const ACTIONS = {
|
|
|
521
521
|
blocks = blocks.filter((b) => remove === 'messages' ? b.kind !== 'message' : remove !== '*' && b.id !== remove);
|
|
522
522
|
}
|
|
523
523
|
cardField(ctx, 'blocked_by', JSON.stringify(blocks));
|
|
524
|
+
// task_links is the Hermes projection of the canonical card relations, including within this transaction.
|
|
525
|
+
run(ctx.db, 'DELETE FROM task_links WHERE child_id = ?', ctx.id);
|
|
526
|
+
for (const block of blocks.filter((b) => b.kind === 'card')) run(ctx.db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', block.id, ctx.id);
|
|
524
527
|
},
|
|
525
528
|
flag(ctx, { name, value, reason = null }) {
|
|
526
529
|
if (!['paused', 'archived'].includes(name)) throw new Error(`unknown flag ${name}`);
|
|
@@ -712,6 +715,12 @@ const ACTIONS = {
|
|
|
712
715
|
send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
|
|
713
716
|
};
|
|
714
717
|
|
|
718
|
+
/** The create door uses the instance's canonical dependency representation. */
|
|
719
|
+
export function addDependency(db, entry, id, parent) {
|
|
720
|
+
if (entry.workflow.relations?.blocked_by) ACTIONS.relation({ db, id }, { add: parent, reason: 'declared dependency' });
|
|
721
|
+
else run(db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', parent, id);
|
|
722
|
+
}
|
|
723
|
+
|
|
715
724
|
/** Persist close intent in the same transaction as the transition; I/O can fail or never run. */
|
|
716
725
|
export function requestSessionClose(db, id, { runId = null, slot = null, through = null } = {}) {
|
|
717
726
|
for (const row of all(db, 'SELECT id, metadata FROM task_runs WHERE task_id = ?', id)) {
|
package/board/ztrack.mjs
CHANGED
|
@@ -523,6 +523,12 @@ class ZtrackBoard {
|
|
|
523
523
|
const compat = new DatabaseSync(this.compatPath);
|
|
524
524
|
try {
|
|
525
525
|
compat.exec('BEGIN'); this.init(compat, { fresh: true });
|
|
526
|
+
// Native events and Hermes events share an export database, not an id sequence.
|
|
527
|
+
// Bootstrap the old identity mapping once; new exports allocate independent Hermes ids.
|
|
528
|
+
if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_export_events'").get()) {
|
|
529
|
+
compat.exec(`CREATE TABLE supercode_export_events (source_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL UNIQUE)`);
|
|
530
|
+
compat.exec(`INSERT INTO supercode_export_events SELECT id, id FROM task_events WHERE json_extract(payload, '$.__supercode_export') = 1`);
|
|
531
|
+
}
|
|
526
532
|
// Every import is event-based. Existing unconsumed acts must never be erased by an export race.
|
|
527
533
|
const cursor = this.get('hermes_cursor', 0);
|
|
528
534
|
const unseen = compat.prepare('SELECT payload FROM task_events WHERE id > ?').all(cursor).some((r) => !json(r.payload, {})?.__supercode_export);
|
|
@@ -535,14 +541,49 @@ class ZtrackBoard {
|
|
|
535
541
|
for (const {name,key,row:source} of sources.values()) {
|
|
536
542
|
if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name)) continue;
|
|
537
543
|
const table = this.rows.tables.get(name);
|
|
544
|
+
if (name === 'task_events') {
|
|
545
|
+
const sourceId = JSON.parse(key)[0];
|
|
546
|
+
const mapped = compat.prepare('SELECT event_id FROM supercode_export_events WHERE source_id = ?').get(sourceId);
|
|
547
|
+
if (!source) {
|
|
548
|
+
if (mapped) compat.prepare('DELETE FROM task_events WHERE id = ?').run(mapped.event_id);
|
|
549
|
+
compat.prepare('DELETE FROM supercode_export_events WHERE source_id = ?').run(sourceId);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const row = { ...source, payload: JSON.stringify({ ...json(source.payload, {}), __supercode_export: true }) };
|
|
553
|
+
delete row.id;
|
|
554
|
+
const keys = Object.keys(row);
|
|
555
|
+
if (mapped) {
|
|
556
|
+
const previous = compat.prepare('SELECT * FROM task_events WHERE id = ?').get(mapped.event_id);
|
|
557
|
+
if (previous && keys.every((k) => previous[k] === row[k])) continue;
|
|
558
|
+
compat.prepare(`UPDATE task_events SET ${keys.map((k) => `${quote(k)} = ?`).join(',')} WHERE id = ?`).run(...keys.map((k) => row[k]), mapped.event_id);
|
|
559
|
+
} else {
|
|
560
|
+
const inserted = compat.prepare(`INSERT INTO task_events (${keys.map(quote)}) VALUES (${keys.map(() => '?')})`).run(...keys.map((k) => row[k]));
|
|
561
|
+
compat.prepare('INSERT INTO supercode_export_events (source_id, event_id) VALUES (?, ?)').run(sourceId, inserted.lastInsertRowid);
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
538
565
|
if (!source) { compat.prepare(`DELETE FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key)); continue; }
|
|
539
566
|
const row = {...source};
|
|
540
|
-
if (name === 'task_events') row.payload = JSON.stringify({...json(row.payload,{}),__supercode_export:true});
|
|
541
567
|
const keys=Object.keys(row);
|
|
542
568
|
const previous=compat.prepare(`SELECT * FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).get(...JSON.parse(key));
|
|
543
569
|
if (previous && keys.every(k=>previous[k]===row[k]))continue;
|
|
544
570
|
compat.prepare(`INSERT OR REPLACE INTO ${quote(name)} (${keys.map(quote)}) VALUES (${keys.map(()=>'?')})`).run(...keys.map(k=>row[k]));
|
|
545
571
|
}
|
|
572
|
+
// Hermes recomputes ready on read. Its sticky block is the last blocked/unblocked event,
|
|
573
|
+
// so lowering the status alone cannot preserve a pause (including one made by a document edit).
|
|
574
|
+
for (const id of projected) {
|
|
575
|
+
const state = compat.prepare('SELECT t.status, c.stage, c.paused, c.blocked_by FROM tasks t JOIN supercode_cards c ON c.task_id = t.id WHERE t.id = ?').get(id);
|
|
576
|
+
if (!state) continue;
|
|
577
|
+
const sticky = state.status === 'blocked' && (json(state.paused, null) || json(state.blocked_by, []).some((b) => b.kind === 'message'));
|
|
578
|
+
const last = compat.prepare("SELECT kind, payload FROM task_events WHERE task_id = ? AND kind IN ('blocked','unblocked') ORDER BY id DESC LIMIT 1").get(id);
|
|
579
|
+
if (sticky) compat.prepare("UPDATE tasks SET block_kind = 'needs_input' WHERE id = ?").run(id);
|
|
580
|
+
const cleared = !sticky && json(last?.payload, {})?.__supercode_sticky;
|
|
581
|
+
if ((sticky && last?.kind !== 'blocked') || (cleared && last?.kind === 'blocked')) {
|
|
582
|
+
const payload = { __supercode_export: true, __supercode_sticky: true, block_kind: 'needs_input', source_status: state.stage,
|
|
583
|
+
reason: json(state.paused, null)?.reason ?? (sticky ? 'waiting on a message' : 'lowered hold cleared') };
|
|
584
|
+
compat.prepare('INSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, ?, ?, ?)').run(id, sticky ? 'blocked' : 'unblocked', JSON.stringify(payload), now());
|
|
585
|
+
}
|
|
586
|
+
}
|
|
546
587
|
compat.exec('COMMIT');
|
|
547
588
|
// Exported events carry an origin marker and never advance the Hermes input cursor.
|
|
548
589
|
} catch (error) { try { compat.exec('ROLLBACK'); } catch {} throw error; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.106",
|
|
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": {
|