@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 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
- if (completionOwnerOf(db, cardId)?.endsWith(`:${sessionId}`)) roles.push('creator');
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
- if (completionOwnerOf(db, card.id)?.endsWith(`:${callerSession()}`)) return; // its declared creator doors still govern the act
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}`);
@@ -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>`] });
@@ -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) 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(' ')}`);
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.139",
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": {