@volter/supercode-orchestrator 0.4.46 → 0.4.48

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
 
@@ -419,7 +422,11 @@ function closeRequested(board, report) {
419
422
  const session = json(row.metadata, {})?.supercode;
420
423
  if (!session?.pane) continue;
421
424
  try { stopSession(board, session); } catch (error) { report.undelivered.push([row.task_id, session.address, `closing it: ${String(error.message).slice(-200)}`]); continue; }
422
- tx(board, (db) => setRunSession(db, row.id, { ...session, closed_at: now() }));
425
+ const address = session.address ?? `sc:${session.machine}:claude-code:${session.session_id}`;
426
+ tx(board, (db) => {
427
+ setRunSession(db, row.id, { ...session, closed_at: now() });
428
+ db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(row.task_id, address);
429
+ });
423
430
  }
424
431
  }
425
432
 
@@ -473,7 +480,12 @@ export async function sessionEffect(board, e) {
473
480
  const caller = process.env.CLAUDE_CODE_SESSION_ID || process.env.CODEX_THREAD_ID;
474
481
  if (caller && caller === session.session_id) { tx(board, (db) => setRunSession(db, row.id, { ...session, close_requested: now() })); return; }
475
482
  stopSession(board, session);
476
- tx(board, (db) => setRunSession(db, row.id, { ...session, closed_at: now() }));
483
+ // a closed session hears nothing more of the card
484
+ const address = session.address ?? `sc:${session.machine}:claude-code:${session.session_id}`;
485
+ tx(board, (db) => {
486
+ setRunSession(db, row.id, { ...session, closed_at: now() });
487
+ db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ?").run(e.id, address);
488
+ });
477
489
  return;
478
490
  }
479
491
  }
@@ -569,7 +581,8 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
569
581
  const prev = tx(board, (db) => lastSession(db, task.id, slot));
570
582
  let session = null;
571
583
  let replaced = null;
572
- if (prev && prev.machine === machine) {
584
+ if (prev?.forgotten) replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
585
+ else if (prev && prev.machine === machine) {
573
586
  if (sessionAlive(fleet, prev)) {
574
587
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
575
588
  const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, ...kept } = prev;
@@ -588,6 +601,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
588
601
  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) };
589
602
  } else replaced = { session_id: prev.session_id, why: 'its resume never came up' };
590
603
  } else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
604
+ // a card keeps its context: a slot's session that cannot be kept or resumed stops the launch, and only a session the
605
+ // workflow let go (`forget_session`) is replaced by a new one
606
+ if (replaced && !prev?.forgotten) {
607
+ 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`);
608
+ failed.resumeFailed = { session_id: replaced.session_id, slot, why: replaced.why };
609
+ throw failed;
610
+ }
591
611
  if (!session) {
592
612
  const sid = randomUUID();
593
613
  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) });
@@ -798,7 +818,8 @@ async function notify(board, fleet, report, entry) {
798
818
  const events = board.db.prepare(`SELECT id, kind, payload, created_at FROM task_events WHERE task_id = ? AND id > ? AND kind IN (${kinds.map(() => '?').join(', ')}) ORDER BY id`)
799
819
  .all(sub.task_id, sub.last_event_id, ...kinds);
800
820
  if (!events.length) return;
801
- const task = tx(board, (db) => getTask(db, sub.task_id));
821
+ // the card as the workflow names it (its status the workflow's: triaging, reviewing, stopped; `stored_as` kanban.db's)
822
+ const task = tx(board, (db) => { const t = getTask(db, sub.task_id); return t ? { ...t, status: statusOf(db, t), stored_as: t.status } : t; });
802
823
  let cursor = Number(sub.last_event_id);
803
824
  let failed = null;
804
825
  try {
@@ -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);
@@ -462,6 +463,18 @@ const ACTIONS = {
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
465
  close_session(ctx, slot) { ctx.effects.push({ kind: 'close_session', id: ctx.id, slot }); },
466
+ // the slot's last session is let go: its next run starts a new session (a card's context is otherwise kept: a
467
+ // session that cannot be resumed stops the card, it is never replaced on the board's own account)
468
+ forget_session(ctx, slot) {
469
+ const name = String(slot ?? 'worker');
470
+ 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)) {
471
+ const whole = json(row.metadata, {}) ?? {};
472
+ if (!whole.supercode?.session_id || (whole.supercode.slot ?? 'worker') !== name) continue;
473
+ whole.supercode = { ...whole.supercode, forgotten: now() };
474
+ run(ctx.db, 'UPDATE task_runs SET metadata = ? WHERE id = ?', JSON.stringify(whole), row.id);
475
+ return;
476
+ }
477
+ },
465
478
  send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
466
479
  };
467
480
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.46",
3
+ "version": "0.4.48",
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": {