@volter/supercode-orchestrator 0.4.105 → 0.4.107
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 +52 -11
- package/board/engine.mjs +3 -3
- package/board/workflow.mjs +9 -0
- package/board/ztrack.mjs +48 -1
- 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) => {
|
|
@@ -299,7 +321,7 @@ function stopSession(board, session) {
|
|
|
299
321
|
}
|
|
300
322
|
if (session.pane) {
|
|
301
323
|
const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
|
|
302
|
-
if (r.code !== 0 ||
|
|
324
|
+
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)}`);
|
|
303
325
|
}
|
|
304
326
|
}
|
|
305
327
|
|
|
@@ -484,11 +506,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
484
506
|
const session = json(row.metadata, {})?.supercode;
|
|
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
|
-
const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM
|
|
488
|
-
if (active.some((r) => {
|
|
489
|
-
const s = json(r.metadata, {})?.supercode;
|
|
490
|
-
return r.id !== row.id && s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
|
|
491
|
-
})) continue;
|
|
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());
|
|
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;
|
|
@@ -501,7 +519,12 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
501
519
|
}
|
|
502
520
|
const observed = panes.get(machine);
|
|
503
521
|
if (observed.error) throw new Error(observed.error);
|
|
504
|
-
if (observed.live.has(session.pane)) {
|
|
522
|
+
if (observed.live.has(session.pane)) {
|
|
523
|
+
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())
|
|
524
|
+
.some((r) => { const s = json(r.metadata, {})?.supercode; return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine); });
|
|
525
|
+
if (protectedNow) continue;
|
|
526
|
+
stopSession(board, session); observed.live.delete(session.pane);
|
|
527
|
+
}
|
|
505
528
|
} catch (e) { error = String(e.message).slice(-500); }
|
|
506
529
|
tx(board, (db) => {
|
|
507
530
|
const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
|
@@ -671,6 +694,16 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
671
694
|
.find((r) => r.session?.pane && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
|
|
672
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'}`);
|
|
673
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
|
+
}
|
|
674
707
|
if (prev?.close_requested && !prev.closed_at) {
|
|
675
708
|
const report = { undelivered: [] };
|
|
676
709
|
closeRequested(board, report, { taskId: task.id });
|
|
@@ -881,6 +914,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
881
914
|
if (!found) throw new Error(`the fleet lists no live session ${who}`);
|
|
882
915
|
const [, host, harness, sessionId] = found.address.split(':');
|
|
883
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
|
+
|
|
884
923
|
// the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
|
|
885
924
|
let dir = workspace;
|
|
886
925
|
if (!dir) {
|
|
@@ -894,7 +933,8 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
894
933
|
return tx(board, (db) => {
|
|
895
934
|
const task = getTask(db, id);
|
|
896
935
|
if (!task) throw new Error(`no card ${id}`);
|
|
897
|
-
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');
|
|
898
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`);
|
|
899
939
|
const was = lastSession(db, id);
|
|
900
940
|
if (was && was.session_id !== sessionId && sessionAlive(fleet, was)) throw new Error(`${id} already has a live session, ${was.address}`);
|
|
@@ -905,11 +945,12 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
|
|
|
905
945
|
db.prepare("UPDATE tasks SET workspace_kind = 'dir', workspace_path = ? WHERE id = ?").run(dir.replace(/^dir:/, ''), id);
|
|
906
946
|
}
|
|
907
947
|
// the workflow's adopt event: the card is claimed for its implementer, whose session this now is
|
|
908
|
-
|
|
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() } });
|
|
909
950
|
const runId = getTask(db, id).current_run_id;
|
|
910
951
|
if (!runId) throw new Error(`${id} waits on parents that are not done`);
|
|
911
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() };
|
|
912
|
-
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 });
|
|
913
954
|
event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt' }, runId);
|
|
914
955
|
subscribeIn(db, id, found.address, 'worker');
|
|
915
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
|
@@ -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,11 +523,23 @@ 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
|
+
// 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
|
+
}
|
|
536
|
+
}
|
|
526
537
|
// Every import is event-based. Existing unconsumed acts must never be erased by an export race.
|
|
527
538
|
const cursor = this.get('hermes_cursor', 0);
|
|
528
539
|
const unseen = compat.prepare('SELECT payload FROM task_events WHERE id > ?').all(cursor).some((r) => !json(r.payload, {})?.__supercode_export);
|
|
529
540
|
if (unseen) throw new Error('Hermes wrote during board export; retained its events for the next read');
|
|
530
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) });
|
|
531
543
|
for (const source of [...sources.values()]) {
|
|
532
544
|
const id = source.name === 'tasks' ? source.row?.id : source.name === 'supercode_cards' ? source.row?.task_id : null;
|
|
533
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)});
|
|
@@ -535,14 +547,49 @@ class ZtrackBoard {
|
|
|
535
547
|
for (const {name,key,row:source} of sources.values()) {
|
|
536
548
|
if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name)) continue;
|
|
537
549
|
const table = this.rows.tables.get(name);
|
|
550
|
+
if (name === 'task_events') {
|
|
551
|
+
const sourceId = JSON.parse(key)[0];
|
|
552
|
+
const mapped = compat.prepare('SELECT event_id FROM supercode_export_events WHERE source_id = ?').get(sourceId);
|
|
553
|
+
if (!source) {
|
|
554
|
+
if (mapped) compat.prepare('DELETE FROM task_events WHERE id = ?').run(mapped.event_id);
|
|
555
|
+
compat.prepare('DELETE FROM supercode_export_events WHERE source_id = ?').run(sourceId);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const row = { ...source, payload: JSON.stringify({ ...json(source.payload, {}), __supercode_export: true }) };
|
|
559
|
+
delete row.id;
|
|
560
|
+
const keys = Object.keys(row);
|
|
561
|
+
if (mapped) {
|
|
562
|
+
const previous = compat.prepare('SELECT * FROM task_events WHERE id = ?').get(mapped.event_id);
|
|
563
|
+
if (previous && keys.every((k) => previous[k] === row[k])) continue;
|
|
564
|
+
compat.prepare(`UPDATE task_events SET ${keys.map((k) => `${quote(k)} = ?`).join(',')} WHERE id = ?`).run(...keys.map((k) => row[k]), mapped.event_id);
|
|
565
|
+
} else {
|
|
566
|
+
const inserted = compat.prepare(`INSERT INTO task_events (${keys.map(quote)}) VALUES (${keys.map(() => '?')})`).run(...keys.map((k) => row[k]));
|
|
567
|
+
compat.prepare('INSERT INTO supercode_export_events (source_id, event_id) VALUES (?, ?)').run(sourceId, inserted.lastInsertRowid);
|
|
568
|
+
}
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
538
571
|
if (!source) { compat.prepare(`DELETE FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key)); continue; }
|
|
539
572
|
const row = {...source};
|
|
540
|
-
if (name === 'task_events') row.payload = JSON.stringify({...json(row.payload,{}),__supercode_export:true});
|
|
541
573
|
const keys=Object.keys(row);
|
|
542
574
|
const previous=compat.prepare(`SELECT * FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).get(...JSON.parse(key));
|
|
543
575
|
if (previous && keys.every(k=>previous[k]===row[k]))continue;
|
|
544
576
|
compat.prepare(`INSERT OR REPLACE INTO ${quote(name)} (${keys.map(quote)}) VALUES (${keys.map(()=>'?')})`).run(...keys.map(k=>row[k]));
|
|
545
577
|
}
|
|
578
|
+
// Hermes recomputes ready on read. Its sticky block is the last blocked/unblocked event,
|
|
579
|
+
// so lowering the status alone cannot preserve a pause (including one made by a document edit).
|
|
580
|
+
for (const id of projected) {
|
|
581
|
+
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);
|
|
582
|
+
if (!state) continue;
|
|
583
|
+
const sticky = state.status === 'blocked' && (json(state.paused, null) || json(state.blocked_by, []).some((b) => b.kind === 'message'));
|
|
584
|
+
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);
|
|
585
|
+
if (sticky) compat.prepare("UPDATE tasks SET block_kind = 'needs_input' WHERE id = ?").run(id);
|
|
586
|
+
const cleared = !sticky && json(last?.payload, {})?.__supercode_sticky;
|
|
587
|
+
if ((sticky && last?.kind !== 'blocked') || (cleared && last?.kind === 'blocked')) {
|
|
588
|
+
const payload = { __supercode_export: true, __supercode_sticky: true, block_kind: 'needs_input', source_status: state.stage,
|
|
589
|
+
reason: json(state.paused, null)?.reason ?? (sticky ? 'waiting on a message' : 'lowered hold cleared') };
|
|
590
|
+
compat.prepare('INSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, ?, ?, ?)').run(id, sticky ? 'blocked' : 'unblocked', JSON.stringify(payload), now());
|
|
591
|
+
}
|
|
592
|
+
}
|
|
546
593
|
compat.exec('COMMIT');
|
|
547
594
|
// Exported events carry an origin marker and never advance the Hermes input cursor.
|
|
548
595
|
} 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.107",
|
|
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": {
|