@volter/supercode-orchestrator 0.4.47 → 0.4.49

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.
@@ -369,7 +369,8 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
369
369
  report.started.push([task.id, session.address ?? `pid ${session.pid}`, session.mode]);
370
370
  }
371
371
  } catch (error) {
372
- try { const r = tx(board, (db) => fire(db, board, entry, task.id, 'spawn_failed', { roles: ['platform'], payload: { error: String(error.message).slice(-500) } })); effects.push(...r.effects); } catch { /* no transition for it */ }
372
+ const payload = { error: String(error.message).slice(-500), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
373
+ try { const r = tx(board, (db) => fire(db, board, entry, task.id, 'spawn_failed', { roles: ['platform'], payload })); effects.push(...r.effects); } catch { /* no transition for it */ }
373
374
  report.failed.push([task.id, String(error.message).slice(-300)]);
374
375
  }
375
376
  }
@@ -388,10 +389,12 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
388
389
  // a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
389
390
  const arcContext = task.subtask_of ? tx(board, (db) => cardContext(db, task.subtask_of)) : '';
390
391
  const prompt = render(spec.prompt, { ...scope, where, context: tx(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
391
- // the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's)
392
- const meta = { status, role: spec.role, slot: spec.session };
392
+ // the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
393
+ // is a template, so a step can ask for isolation by naming a new one
394
+ const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
395
+ const meta = { status, role: spec.role, slot };
393
396
  return surface === 'pane'
394
- ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot: spec.session, meta })
397
+ ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
395
398
  : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
396
399
  }
397
400
 
@@ -471,6 +474,8 @@ export async function sessionEffect(board, e) {
471
474
  if (e.kind === 'close_session') {
472
475
  const rows = tx(board, (db) => db.prepare('SELECT id, metadata FROM task_runs WHERE task_id = ? ORDER BY id DESC').all(e.id));
473
476
  for (const row of rows) {
477
+ // a run newer than the one that left the status is its successor, already started in this tick: not closed
478
+ if (e.runId && row.id > e.runId) continue;
474
479
  const session = json(row.metadata, {})?.supercode;
475
480
  if ((session?.slot ?? 'worker') !== e.slot || !session?.pane || session.closed_at) continue;
476
481
  // the session that sent the event is not closed under its own command: the dispatcher closes it next tick
@@ -578,10 +583,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
578
583
  const prev = tx(board, (db) => lastSession(db, task.id, slot));
579
584
  let session = null;
580
585
  let replaced = null;
581
- if (prev && prev.machine === machine) {
582
- if (sessionAlive(fleet, prev)) {
586
+ if (prev?.forgotten) replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
587
+ else if (prev && prev.machine === machine) {
588
+ // a session the board closed is not kept, whatever a fleet reading taken before the close still lists
589
+ if (!prev.closed_at && sessionAlive(fleet, prev)) {
583
590
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
584
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, ...kept } = prev;
591
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, close_requested: _q, closed_at: _c, ...kept } = prev;
585
592
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
586
593
  // a kept session is told what this run is for (its card came back to it, with what happened since)
587
594
  if (input) deliverNow(board, prev.address ?? `sc:${prev.machine}:claude-code:${prev.session_id}`, input, `board-run-${task.id}-r${runId}`);
@@ -597,6 +604,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
597
604
  else replaced = { session_id: prev.session_id, why: r.code === 0 ? `its resume ended as soon as it started (pane ${pane})` : (r.stderr || r.stdout).trim().slice(-300) };
598
605
  } else replaced = { session_id: prev.session_id, why: 'its resume never came up' };
599
606
  } else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
607
+ // a card keeps its context: a slot's session that cannot be kept or resumed stops the launch, and only a session the
608
+ // workflow let go (`forget_session`) is replaced by a new one
609
+ if (replaced && !prev?.forgotten) {
610
+ const failed = new Error(`session ${replaced.session_id} (${slot}) could not be continued: ${replaced.why}. The board keeps a card's context and starts a new session only when the workflow lets this one go`);
611
+ failed.resumeFailed = { session_id: replaced.session_id, slot, why: replaced.why };
612
+ throw failed;
613
+ }
600
614
  if (!session) {
601
615
  const sid = randomUUID();
602
616
  const r = sc([...base, '--key', `board-${task.id}-r${runId}-${sid.slice(0, 8)}`, '--input', input ?? `Card ${task.id}: ${task.title}`, '--', '--session-id', sid, ...tail], { timeout: 180_000, as: boardAddress(board.label) });
@@ -414,9 +414,10 @@ const ACTIONS = {
414
414
  }
415
415
  const expires = t + ttl;
416
416
  const profile = evaluate(ctx.entry.workflow.roles?.[role] ?? 'card.assignee', scope) ?? task.assignee;
417
+ const slot = String(templateValue(ctx.entry.workflow.statuses[ctx.to]?.run?.session ?? 'worker', scope) ?? 'worker');
417
418
  run(ctx.db, 'UPDATE tasks SET claim_lock = ?, claim_expires = ?, started_at = COALESCE(started_at, ?) WHERE id = ?', lock, expires, t, ctx.id);
418
419
  const r = run(ctx.db, "INSERT INTO task_runs (task_id, profile, step_key, status, claim_lock, claim_expires, max_runtime_seconds, started_at, metadata) VALUES (?, ?, ?, 'running', ?, ?, ?, ?, ?)",
419
- ctx.id, profile, ctx.to, lock, expires, task.max_runtime_seconds, t, JSON.stringify({ supercode: { role, status: ctx.to, slot: ctx.entry.workflow.statuses[ctx.to]?.run?.session ?? 'worker' } }));
420
+ ctx.id, profile, ctx.to, lock, expires, task.max_runtime_seconds, t, JSON.stringify({ supercode: { role, status: ctx.to, slot } }));
420
421
  ctx.runId = Number(r.lastInsertRowid);
421
422
  run(ctx.db, 'UPDATE tasks SET current_run_id = ? WHERE id = ?', ctx.runId, ctx.id);
422
423
  record(ctx.db, ctx.id, 'claimed', { lock, expires, run_id: ctx.runId, source_status: resumeOf(ctx.entry.workflow, ctx.to) }, ctx.runId);
@@ -461,7 +462,20 @@ const ACTIONS = {
461
462
  },
462
463
  cleanup_workspace(ctx) { ctx.effects.push({ kind: 'cleanup_workspace', id: ctx.id }); },
463
464
  stop_run(ctx) { ctx.effects.push({ kind: 'stop_run', id: ctx.id, runId: ctx.runId }); },
464
- close_session(ctx, slot) { ctx.effects.push({ kind: 'close_session', id: ctx.id, slot }); },
465
+ // the run leaving the status is the one whose session closes, never a run started after it (a reclaim's successor)
466
+ close_session(ctx, slot) { ctx.effects.push({ kind: 'close_session', id: ctx.id, slot, runId: ctx.runId }); },
467
+ // the slot's last session is let go: its next run starts a new session (a card's context is otherwise kept: a
468
+ // session that cannot be resumed stops the card, it is never replaced on the board's own account)
469
+ forget_session(ctx, slot) {
470
+ const name = String(slot ?? 'worker');
471
+ for (const row of all(ctx.db, 'SELECT id, metadata FROM task_runs WHERE task_id = ? AND metadata IS NOT NULL ORDER BY id DESC', ctx.id)) {
472
+ const whole = json(row.metadata, {}) ?? {};
473
+ if (!whole.supercode?.session_id || (whole.supercode.slot ?? 'worker') !== name) continue;
474
+ whole.supercode = { ...whole.supercode, forgotten: now() };
475
+ run(ctx.db, 'UPDATE task_runs SET metadata = ? WHERE id = ?', JSON.stringify(whole), row.id);
476
+ return;
477
+ }
478
+ },
465
479
  send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
466
480
  };
467
481
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.47",
3
+ "version": "0.4.49",
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": {