@volter/supercode-orchestrator 0.4.139 → 0.4.141
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 +7 -5
- package/board/dispatch.mjs +19 -7
- package/board/engine.mjs +12 -2
- package/board/workflow.mjs +6 -14
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import { basename, join, resolve } from 'node:path';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { read } from '../orchestration.mjs';
|
|
12
|
-
import { view, backingPaths, layOut, backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
12
|
+
import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
13
13
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
14
14
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
15
15
|
import * as engine from './engine.mjs';
|
|
@@ -385,7 +385,7 @@ export async function runWorkflow(argv) {
|
|
|
385
385
|
const card = withBoard((b) => {
|
|
386
386
|
const got = engine.showCard(b.db, id);
|
|
387
387
|
got.runs = engine.runs(b.db, id, { stateType: args['state-type'] ?? null, stateName: args['state-name'] ?? null });
|
|
388
|
-
got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit });
|
|
388
|
+
got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit, launchWindowSeconds: settings.launchWindow });
|
|
389
389
|
got.stage = statusOf(b.db, got.task);
|
|
390
390
|
got.time = scopeOf(b.db, b, got.task, { params: workflowFor(root).params }).card.time;
|
|
391
391
|
return got;
|
|
@@ -644,7 +644,7 @@ export async function runWorkflow(argv) {
|
|
|
644
644
|
const order = ['warning', 'error', 'critical'];
|
|
645
645
|
const found = withBoard((b) => {
|
|
646
646
|
const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
|
|
647
|
-
return cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))
|
|
647
|
+
return cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))
|
|
648
648
|
.filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
|
|
649
649
|
});
|
|
650
650
|
if (args.json) { say(args, '', found); return 0; }
|
|
@@ -1090,7 +1090,8 @@ function rolesFor(db, cardId, sessionId, params = {}) {
|
|
|
1090
1090
|
const task = engine.getTask(db, cardId);
|
|
1091
1091
|
if (!task) return [];
|
|
1092
1092
|
const roles = [];
|
|
1093
|
-
|
|
1093
|
+
const creator = completionOwnerOf(db, cardId);
|
|
1094
|
+
if (creator && creator === actorOf()) roles.push('creator');
|
|
1094
1095
|
const current = task.current_run_id ? json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(task.current_run_id)?.metadata, {})?.supercode : null;
|
|
1095
1096
|
if (current?.session_id === sessionId) roles.push(current.role ?? 'implementer');
|
|
1096
1097
|
else if (engine.lastSession(db, cardId)?.session_id === sessionId) roles.push('implementer');
|
|
@@ -1111,7 +1112,8 @@ function authorize(db, verb, rest, args, own) {
|
|
|
1111
1112
|
if (args.all) refuse(`does not ${verb} every card; only its own`);
|
|
1112
1113
|
const card = engine.getTask(db, rest[0]);
|
|
1113
1114
|
if (!card) return; // the verb itself answers "no card"
|
|
1114
|
-
|
|
1115
|
+
const creator = completionOwnerOf(db, card.id);
|
|
1116
|
+
if (creator && creator === actorOf()) return;
|
|
1115
1117
|
// a card's session writes its own card and its subtasks; what it may do with them is the workflow's (`by`)
|
|
1116
1118
|
if (card.id !== own.id && card.subtask_of !== own.id) refuse(`writes only its own card and its subtasks, not ${card.id}`);
|
|
1117
1119
|
for (const other of args.ids ?? []) if (other !== own.id && engine.getTask(db, other)?.subtask_of !== own.id) refuse(`writes only its own card and its subtasks, not ${other}`);
|
package/board/dispatch.mjs
CHANGED
|
@@ -195,6 +195,7 @@ export function boardSettings(params = {}) {
|
|
|
195
195
|
failureLimit: pos(params.failure_limit) ?? DEFAULT_FAILURE_LIMIT,
|
|
196
196
|
ttl: claimTtl(params.claim_ttl_seconds),
|
|
197
197
|
grace: pos(params.session_grace_seconds) ?? 120,
|
|
198
|
+
launchWindow: pos(params.session_launch_window_seconds) ?? 300,
|
|
198
199
|
interval: Math.max(Number(params.dispatch_interval_seconds) || 60, 1),
|
|
199
200
|
idleQuiet: pos(params.session_idle_quiet_seconds) ?? 600,
|
|
200
201
|
logRotateBytes: pos(params.worker_log_rotate_bytes) ?? 2 * 1024 * 1024,
|
|
@@ -258,10 +259,6 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
258
259
|
if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
|
|
259
260
|
const runRow = currentRun(db, id);
|
|
260
261
|
let session = runRow?.metadata?.supercode ?? null;
|
|
261
|
-
if (session?.launch_pending || session?.launch_deferred) {
|
|
262
|
-
extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
|
|
263
|
-
return; // An uncertain launch owns this run until a machine receipt settles it.
|
|
264
|
-
}
|
|
265
262
|
if (session?.pane) {
|
|
266
263
|
let found;
|
|
267
264
|
try { found = continuationPane(fleet, session, observed.get(session.machine)); }
|
|
@@ -286,6 +283,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
286
283
|
}
|
|
287
284
|
};
|
|
288
285
|
const limits = limitsOf(db, board, entry, task, session);
|
|
286
|
+
if (runRow && (session?.launch_pending || session?.launch_deferred || (!session?.session_id && !session?.pid))) {
|
|
287
|
+
const age = t - Number(session?.launched_at ?? runRow.started_at);
|
|
288
|
+
if (age >= settings.launchWindow) {
|
|
289
|
+
const reason = session?.launch_deferred_error ?? session?.launch_error ?? 'no session or process was bound to the dispatched run';
|
|
290
|
+
const error = `launch window ${settings.launchWindow}s expired after ${age}s: ${reason}`;
|
|
291
|
+
if (!session?.launch_expired_at) {
|
|
292
|
+
setRunSession(db, runRow.id, { launch_expired_at: t, launch_error: error });
|
|
293
|
+
event(db, id, 'session_launch_expired', { error, age_seconds: age, window_seconds: settings.launchWindow, key: session?.launch_key ?? null, pane: session?.pane ?? null }, runRow.id);
|
|
294
|
+
}
|
|
295
|
+
observe('spawn_failed', { error, launch_window_seconds: settings.launchWindow }, null);
|
|
296
|
+
report.failed.push([id, error]);
|
|
297
|
+
} else extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
289
300
|
// Availability is the same retained linked/unlinked feed subscribers hear.
|
|
290
301
|
// A failed stream is unknown, not evidence that every remote machine left.
|
|
291
302
|
if (session?.session_id && session.surface !== 'headless') {
|
|
@@ -681,7 +692,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
681
692
|
if (observed.error) throw new Error(observed.error);
|
|
682
693
|
if (observed.live.has(session.pane)) {
|
|
683
694
|
const pendingNotice = view(board, (db) => db.prepare("SELECT id FROM supercode_effects WHERE json_extract(effect, '$.kind')='session_notice' AND json_extract(effect, '$.runId')=? LIMIT 1").get(row.id));
|
|
684
|
-
if (pendingNotice || session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${session.lifecycle_notice_pending} has not been delivered: ${session.lifecycle_notice_error ?? 'pending'}`);
|
|
695
|
+
if (pendingNotice || session.lifecycle_notice_pending) throw new Error(`close deferred: lifecycle notice ${pendingNotice?.id ?? session.lifecycle_notice_pending} has not been delivered: ${session.lifecycle_notice_error ?? 'pending'}`);
|
|
685
696
|
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())
|
|
686
697
|
.some((r) => { const s = json(r.metadata, {})?.supercode; return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine); });
|
|
687
698
|
if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
|
|
@@ -951,8 +962,9 @@ export function reconcileCallerLaunch(board, sid) {
|
|
|
951
962
|
}
|
|
952
963
|
|
|
953
964
|
function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
|
|
954
|
-
const deferred=view(board,db=>db.prepare("SELECT t.id FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
|
|
955
|
-
for(const {id} of deferred) {
|
|
965
|
+
const deferred=view(board,db=>db.prepare("SELECT t.id,r.started_at FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
|
|
966
|
+
for(const {id,started_at} of deferred) {
|
|
967
|
+
if (now() - started_at >= settings.launchWindow) continue;
|
|
956
968
|
try {
|
|
957
969
|
const task=getTaskNow(board,id),status=view(board,db=>statusOf(db,task)),spec=runSpec(entry,status,task);
|
|
958
970
|
const profile=spec&&view(board,db=>evaluate(entry.workflow.roles?.[spec.role]??'card.assignee',scopeOf(db,board,task,{params:entry.params})));
|
package/board/engine.mjs
CHANGED
|
@@ -205,7 +205,7 @@ export function createCard(board, spec) {
|
|
|
205
205
|
tenant, idempotency_key, max_runtime_seconds, skills, max_retries, model_override, provider_override, reasoning_effort, session_id,
|
|
206
206
|
project_id, goal_mode, goal_max_turns)
|
|
207
207
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
208
|
-
id, title, spec.body ?? null, spec.assignee || null, 'todo', Number(spec.priority) || 0, spec.createdBy ?? 'user', t, ws.kind, ws.path,
|
|
208
|
+
id, title, spec.body ?? null, spec.assignee || null, 'todo', Number(spec.priority) || 0, actorOf() ?? spec.createdBy ?? 'user', t, ws.kind, ws.path,
|
|
209
209
|
spec.branch ?? null, spec.tenant ?? null, spec.idempotencyKey ?? null, spec.maxRuntime ?? null,
|
|
210
210
|
spec.skills?.length ? JSON.stringify(spec.skills) : null, spec.maxRetries ?? null, spec.model ?? null, spec.provider ?? null,
|
|
211
211
|
spec.reasoning ?? null, spec.sessionId ?? null, spec.project ?? null, spec.goal ? 1 : 0, spec.goal?.maxTurns ?? null);
|
|
@@ -710,11 +710,21 @@ const hours = (s) => s / 3600;
|
|
|
710
710
|
* same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
|
|
711
711
|
* actions }`, worst first.
|
|
712
712
|
*/
|
|
713
|
-
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null } = {}) {
|
|
713
|
+
export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300 } = {}) {
|
|
714
714
|
const out = [];
|
|
715
715
|
const t = now();
|
|
716
716
|
const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
|
|
717
717
|
const failures = Number(task.consecutive_failures || 0);
|
|
718
|
+
const active = task.current_run_id ? one(db, 'SELECT id,started_at,metadata FROM task_runs WHERE id=?', task.current_run_id) : null;
|
|
719
|
+
const session = json(active?.metadata, {})?.supercode;
|
|
720
|
+
if (active && (session?.launch_pending || session?.launch_deferred || (!session?.session_id && !session?.pid))) {
|
|
721
|
+
const age = t - Number(session?.launched_at ?? active.started_at);
|
|
722
|
+
const reason = session?.launch_deferred_error ?? session?.launch_error ?? 'no session or process has been bound';
|
|
723
|
+
out.push({ kind: 'session_launch_pending', severity: age >= launchWindowSeconds ? 'error' : 'warning',
|
|
724
|
+
title: `run ${active.id} has no confirmed session after ${age}s: ${reason}`,
|
|
725
|
+
data: { run_id: active.id, age_seconds: age, window_seconds: launchWindowSeconds, reason, launch_key: session?.launch_key ?? null, pane: session?.pane ?? null },
|
|
726
|
+
actions: [`supercode workflow log ${task.id}`] });
|
|
727
|
+
}
|
|
718
728
|
if (failures >= Math.max(1, (task.max_retries ?? failureLimit) - 1) && task.status !== 'done') {
|
|
719
729
|
out.push({ kind: 'repeated_failures', severity: task.status === 'blocked' ? 'error' : 'warning', title: `${failures} consecutive failures`,
|
|
720
730
|
data: { failures, last_error: task.last_failure_error }, actions: [`supercode workflow reclaim ${task.id}`, `supercode workflow reassign ${task.id} <profile>`] });
|
package/board/workflow.mjs
CHANGED
|
@@ -66,20 +66,7 @@ export function creatorOf(db, id) {
|
|
|
66
66
|
return payload.source !== 'document' && /^(?:sc:[^:]+:[^:]+:|hermes-run:)/.test(address ?? '') ? address : null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Open subtasks are the current parent worker's plan. Keep creation history
|
|
70
|
-
* immutable while transferring its creator doors when that worker is replaced. */
|
|
71
69
|
export function completionOwnerOf(db, id) {
|
|
72
|
-
const task = one(db, 'SELECT t.status,c.subtask_of FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?', id);
|
|
73
|
-
if (task?.subtask_of && !['done', 'archived'].includes(task.status)) {
|
|
74
|
-
for (const row of all(db, 'SELECT metadata FROM task_runs WHERE task_id=? ORDER BY id DESC', task.subtask_of)) {
|
|
75
|
-
const session = json(row.metadata, {})?.supercode;
|
|
76
|
-
if (!session || (session.slot ?? 'worker') !== 'worker') continue;
|
|
77
|
-
// An unbound replacement leaves authority with the manager; it never
|
|
78
|
-
// revives the previous worker's ability to finish the new worker's plan.
|
|
79
|
-
return session.address ?? (session.machine && session.harness && session.session_id
|
|
80
|
-
? `sc:${session.machine}:${session.harness}:${session.session_id}` : null);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
70
|
return creatorOf(db, id);
|
|
84
71
|
}
|
|
85
72
|
|
|
@@ -377,7 +364,12 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
377
364
|
// `sender`: the roles the event was sent with (a validator can ask who is asking)
|
|
378
365
|
const scope = scopeOf(db, board, task, { params, event: payload, extra: { lanes: lanesIn(entry), ...extra, sender: roles } });
|
|
379
366
|
const allowed = candidates.filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r)));
|
|
380
|
-
if (!allowed.length)
|
|
367
|
+
if (!allowed.length) {
|
|
368
|
+
const state = name === 'complete' ? 'done' : name === 'cancel' ? 'cancelled' : null;
|
|
369
|
+
const request = state && candidates.some(transition => transition.by?.includes('creator'))
|
|
370
|
+
? ` Ask the manager with workflow request-state ${id} --state ${state} --reason <why> --evidence <verdict-and-pointers>.` : '';
|
|
371
|
+
throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} there is for ${[...new Set(candidates.flatMap((t) => t.by ?? []))].join(' or ')}. ${candidates.map((t) => t.description).filter(Boolean).join(' ')}${request}`);
|
|
372
|
+
}
|
|
381
373
|
const taken = allowed.find((t) => holds(t.guard ?? null, scope));
|
|
382
374
|
if (!taken) {
|
|
383
375
|
const state = scope.card;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.141",
|
|
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": {
|