@volter/supercode-orchestrator 0.4.115 → 0.4.117

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.
@@ -525,10 +525,6 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
525
525
  return lane ? launchRun(board, entry, id, lane, fleet, settings) : null;
526
526
  }
527
527
 
528
- /**
529
- * The heartbeat, observed: a running pane session whose last message is a whole tick old is sent `session_idle` with
530
- * how many times it has been asked this run and whether it worked since the last ask; the workflow decides the rest.
531
- */
532
528
  /** Reconcile durable close intent and ended runs, including old unverified closed_at records.
533
529
  * Reads and process I/O stay outside the writer. An active successor owns its pane until it ends. */
534
530
  export function closeRequested(board, report, { taskId = null, caller = null } = {}) {
@@ -611,6 +607,8 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
611
607
  }
612
608
  }
613
609
 
610
+ /** Quiet time is independent of dispatch cadence and the harness's busy label.
611
+ * Keep bounded readings on the run so an ordinary quiet stretch can be reviewed later. */
614
612
  function observeIdle(board, fleet, settings, report, entry, effects) {
615
613
  if (!fleet.ok) return;
616
614
  for (const { id } of board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all()) {
@@ -622,22 +620,44 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
622
620
  const session = runRow?.metadata?.supercode;
623
621
  if (!session?.session_id || session.surface !== 'pane') return;
624
622
  const live = fleet.sessions.find((x) => x.address?.endsWith(`:${session.session_id}`));
625
- // a supercode that reports no last message time gives no reading, and no reading gives no verdict
623
+ const observedAt = now();
626
624
  const lastAt = Math.floor(Number(live?.last_message_at_ms) / 1000);
627
- if (!(lastAt > 0)) return;
628
- if (now() - lastAt < settings.idleQuiet) return;
629
- if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.idleQuiet) return;
625
+ const reading = {
626
+ session_id: session.session_id, observed_at: observedAt,
627
+ last_message_at: lastAt > 0 ? lastAt : null,
628
+ last_message_at_ms: lastAt > 0 ? Number(live.last_message_at_ms) : null,
629
+ status: live?.status ?? null, quiet_seconds: lastAt > 0 ? observedAt - lastAt : null,
630
+ threshold_seconds: settings.idleQuiet, turns_before: Number(session.goal_turns ?? 0),
631
+ };
632
+ const retain = (decision, extra = {}) => {
633
+ const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
634
+ const patch = { idle_observation: observation };
635
+ if (decision === 'below_threshold') patch.idle_below_threshold = observation;
636
+ if (decision === 'dispatched' || decision === 'dispatch_failed') patch.idle_eligible = observation;
637
+ setRunSession(db, runRow.id, patch);
638
+ return observation;
639
+ };
640
+ // No timestamp is no reading, and no reading gives no verdict.
641
+ if (!(lastAt > 0)) { retain('no_timestamp'); return; }
642
+ if (reading.quiet_seconds < settings.idleQuiet) { retain('below_threshold'); return; }
643
+ if (session.goal_nudged_at && observedAt - session.goal_nudged_at < settings.idleQuiet) { retain('nudge_cooldown'); return; }
630
644
  const worked = Boolean(session.goal_nudged_at) && lastAt > session.goal_nudged_at;
631
645
  try {
632
646
  const where = whereOf(board, task);
633
647
  // the ask names what the session may do, as its run's prompt did
634
648
  const said = doors(entry.workflow, status, session.role ?? runSpec(entry, status)?.role ?? 'implementer', id, where, session.surface === 'pane' ? 'pane' : 'headless');
635
- const r = fire(db, board, entry, id, 'session_idle', { roles: ['platform'], payload: { turns: Number(session.goal_turns ?? 0), worked }, extra: { where, doors: said } });
649
+ const r = fire(db, board, entry, id, 'session_idle', { roles: ['platform'], payload: { ...reading, turns: reading.turns_before, worked }, extra: { where, doors: said } });
636
650
  effects.push(...r.effects);
637
651
  const after = currentRun(db, id);
638
- if (after?.id === runRow.id) setRunSession(db, runRow.id, { ...(after.metadata?.supercode ?? session), goal_nudged_at: now() });
652
+ const stored = json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(runRow.id)?.metadata, {})?.supercode;
653
+ const observation = retain('dispatched', { turns_after: Number(stored?.goal_turns ?? 0), nudge_queued: r.effects.some(e => e.kind === 'send_to_actor'), run_ended: after?.id !== runRow.id });
654
+ event(db, id, 'session_idle_observed', observation, runRow.id);
655
+ if (after?.id === runRow.id) setRunSession(db, runRow.id, { goal_nudged_at: observedAt });
639
656
  report.notified.push([id, 'session_idle', live.address]);
640
- } catch (error) { report.failed.push([id, `session_idle: ${String(error.message).slice(-200)}`]); }
657
+ } catch (error) {
658
+ retain('dispatch_failed', { error: String(error.message).slice(-200) });
659
+ report.failed.push([id, `session_idle: ${String(error.message).slice(-200)}`]);
660
+ }
641
661
  });
642
662
  }
643
663
  }
@@ -927,7 +947,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
927
947
  if (paneLive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
928
948
  if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
929
949
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
930
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, ...kept } = prev;
950
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, ...kept } = prev;
931
951
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
932
952
  // a kept session is told what this run is for (its card came back to it, with what happened since)
933
953
  tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
package/board/store.mjs CHANGED
@@ -205,7 +205,7 @@ export function setActor(address) { actor = address || null; }
205
205
  export function event(db, taskId, kind, payload = null, runId = null) {
206
206
  // Session observations belong to the run machine. Legacy runs without a machine state retain
207
207
  // their Hermes event stream; a new instance never sends supervision as card notices.
208
- if (runId && ['claimed', 'spawned', 'session_started', 'session_resumed', 'session_replaced', 'session_adopted', 'claim_extended'].includes(kind)) {
208
+ if (runId && ['claimed', 'spawned', 'session_started', 'session_resumed', 'session_replaced', 'session_adopted', 'claim_extended', 'session_idle_observed'].includes(kind)) {
209
209
  const row = db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(runId);
210
210
  const meta = json(row?.metadata, {}) ?? {};
211
211
  if (meta.supercode?.state) {
@@ -24,6 +24,7 @@ export function declareWorkspace(kind, value, machine, branch = null) {
24
24
  // A legacy path supplied explicitly is checked and registered on its machine, never copied into the card.
25
25
  const info = workspaceDoor(machine, 'inspect', value);
26
26
  if (kind === 'worktree' && info.linked) throw new Error('worktree workspace already names a linked worktree; name its repository for a solo worktree');
27
+ if (kind === 'dir' && !info.branch && !branch) throw new Error('a detached checkout has no portable branch identity; name the repository and a solo worktree branch');
27
28
  const row = workspaceDoor(machine, 'register', value);
28
29
  return { repository: row.repository, branch: branch ?? (kind === 'dir' ? row.branch : null), path: kind === 'dir' ? row.path : null };
29
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.115",
3
+ "version": "0.4.117",
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": {