@volter/supercode-orchestrator 0.4.26 → 0.4.28

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.
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
23
23
  import { boardPath, event, json, listBoards, now, openBoard, tx } from './store.mjs';
24
24
  import { holdBoardTickLock } from '../hermes-locks.mjs';
25
25
  import {
26
- cardContext, claimIn, claimTtl, currentRun, DEFAULT_FAILURE_LIMIT, extendClaim, getTask, lastSession,
26
+ blockIn, cardContext, claimIn, claimTtl, currentRun, DEFAULT_FAILURE_LIMIT, extendClaim, getTask, lastSession,
27
27
  recomputeReady, recordFailure, release, setRunSession, subscribeIn, subscriptions,
28
28
  } from './engine.mjs';
29
29
 
@@ -475,7 +475,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null } = {}) {
475
475
  if (prev && prev.machine === machine) {
476
476
  if (sessionAlive(fleet, prev)) {
477
477
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
478
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _i, worked_since_ask: _w, ...kept } = prev;
478
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, ...kept } = prev;
479
479
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
480
480
  } else if (prev.resumable !== false) {
481
481
  const r = sc([...base, '--key', `board-${task.id}-r${runId}-resume`, '--input',
@@ -915,8 +915,10 @@ function readMailbox(board, address, report) {
915
915
  * The board's heartbeat for a pane session: a running card's session never sits idle without deciding. Each tick, a
916
916
  * session that is idle while its card is still running is asked to decide: `request-review` when the work is done,
917
917
  * `block` with its kind when it cannot go on (waiting on a person, a capability, another card), or keep working. A
918
- * goal card (Hermes's goal loop) is told to keep going. A session is asked once it has been idle a whole tick; one
919
- * seen working since the last ask answered it, and its count starts again. After the turn budget (`goal_max_turns`, else
918
+ * goal card (Hermes's goal loop) is told to keep going. Idle is judged by the session's last message time (supercode
919
+ * reads it from the transcript), never by its status label: a session is asked once its last message is a whole tick
920
+ * old; one whose messages ran on a whole tick past the last ask answered it by working, and its count starts again
921
+ * (a reply to the ask alone is not work). After the turn budget (`goal_max_turns`, else
920
922
  * `kanban.goal_max_turns`) of asks in a row with no work and no decision (a goal card: of turns), the card is blocked `needs_input` and its creator is told.
921
923
  * Whether the work is done is the session's call through its card, not an auxiliary judge's.
922
924
  */
@@ -930,25 +932,22 @@ function pursueGoals(board, fleet, settings, report) {
930
932
  const session = runRow?.metadata?.supercode;
931
933
  if (!session?.session_id || session.surface !== 'pane') return null;
932
934
  const live = fleet.sessions.find((x) => x.address?.endsWith(`:${session.session_id}`));
933
- if (!live) return null;
934
- // a session seen working since the last ask answered it by working: its count starts again (a goal card keeps
935
- // Hermes's budget of turns); one idle for less than a whole tick is between turns, not idle
936
- if (live.status !== 'idle') {
937
- if (session.idle_since || (session.goal_nudged_at && !session.worked_since_ask)) setRunSession(db, runRow.id, { ...session, idle_since: null, worked_since_ask: true });
938
- return null;
939
- }
940
- if (!session.idle_since) { setRunSession(db, runRow.id, { ...session, idle_since: now() }); return null; }
941
- if (now() - session.idle_since < settings.interval) return null;
935
+ // a supercode that reports no last message time gives no reading, and no reading gives no verdict
936
+ const lastAt = Math.floor(Number(live.last_message_at_ms) / 1000);
937
+ if (!(lastAt > 0)) return null;
938
+ // a message within the last tick is a session working or between turns, not idle
939
+ if (now() - lastAt < settings.interval) return null;
942
940
  if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.interval * 2) return null;
943
- const turns = session.worked_since_ask && !task.goal_mode ? 0 : Number(session.goal_turns ?? 0);
941
+ // messages that ran on a whole tick past the last ask answered it by working: the count starts again (a goal
942
+ // card keeps Hermes's budget of turns)
943
+ const worked = Boolean(session.goal_nudged_at) && lastAt >= session.goal_nudged_at + settings.interval;
944
+ const turns = worked && !task.goal_mode ? 0 : Number(session.goal_turns ?? 0);
944
945
  const budget = task.goal_max_turns ?? settings.goalMaxTurns;
945
946
  if (turns >= budget) {
946
- db.prepare("UPDATE tasks SET status = 'blocked', claim_lock = NULL, claim_expires = NULL WHERE id = ?").run(id);
947
- const ended = endRun(db, id, { outcome: 'blocked', summary: `idle ${budget} times without deciding done or blocked` });
948
- event(db, id, 'blocked', { reason: `its session went idle ${budget} times without deciding done or blocked; review it`, kind: 'needs_input', source_status: 'running' }, ended);
947
+ blockIn(db, id, { reason: `its session went idle ${budget} times without deciding done or blocked; review it`, kind: 'needs_input' });
949
948
  return null;
950
949
  }
951
- setRunSession(db, runRow.id, { ...session, goal_turns: turns + 1, goal_nudged_at: now(), worked_since_ask: false });
950
+ setRunSession(db, runRow.id, { ...session, goal_turns: turns + 1, goal_nudged_at: now() });
952
951
  event(db, id, task.goal_mode ? 'goal_continued' : 'decision_asked', { turn: turns + 1, budget }, runRow.id);
953
952
  const local = normalMachine(task.machine) === localMachine();
954
953
  const where = local ? `--root ${board.root}${board.slug !== 'default' ? ` --board ${board.slug}` : ''}` : `--at ${boardAddress(board.label)}`;
package/board/engine.mjs CHANGED
@@ -396,25 +396,28 @@ function completeIn(board, id, { result = null, summary = null, metadata = null
396
396
  }
397
397
 
398
398
  export function block(board, id, { reason = null, kind = null } = {}) {
399
+ return tx(board, (db) => blockIn(db, id, { reason, kind }));
400
+ }
401
+
402
+ /** `block` inside the caller's transaction (the dispatcher's own blocks keep the same recurrence accounting). */
403
+ export function blockIn(db, id, { reason = null, kind = null } = {}) {
399
404
  if (kind && !BLOCK_KINDS.includes(kind)) throw new Error(`block kind is one of ${BLOCK_KINDS.join(', ')}`);
400
- return tx(board, (db) => {
401
- const task = must(db, id);
402
- if (!['running', 'ready'].includes(task.status)) throw new Error(`${id} is ${task.status}; only a running or ready card blocks`);
403
- const source = task.status === 'running' ? retryStatus(db, id) : 'ready';
404
- if (kind === 'dependency') {
405
- run(db, "UPDATE tasks SET status = 'todo', claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ? WHERE id = ?", kind, id);
406
- const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
407
- event(db, id, 'dependency_wait', { reason, kind, source_status: source }, runId);
408
- return 'todo';
409
- }
410
- const recurrences = task.block_kind === kind ? Number(task.block_recurrences || 0) + 1 : 1;
411
- const loop = recurrences >= BLOCK_RECURRENCE_LIMIT;
412
- const status = loop ? 'triage' : 'blocked';
413
- run(db, 'UPDATE tasks SET status = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ?, block_recurrences = ? WHERE id = ?', status, kind, recurrences, id);
405
+ const task = must(db, id);
406
+ if (!['running', 'ready'].includes(task.status)) throw new Error(`${id} is ${task.status}; only a running or ready card blocks`);
407
+ const source = task.status === 'running' ? retryStatus(db, id) : 'ready';
408
+ if (kind === 'dependency') {
409
+ run(db, "UPDATE tasks SET status = 'todo', claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ? WHERE id = ?", kind, id);
414
410
  const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
415
- event(db, id, loop ? 'block_loop_detected' : 'blocked', { reason, kind, recurrences, ...(loop ? { limit: BLOCK_RECURRENCE_LIMIT } : {}), source_status: source }, runId);
416
- return status;
417
- });
411
+ event(db, id, 'dependency_wait', { reason, kind, source_status: source }, runId);
412
+ return 'todo';
413
+ }
414
+ const recurrences = task.block_kind === kind ? Number(task.block_recurrences || 0) + 1 : 1;
415
+ const loop = recurrences >= BLOCK_RECURRENCE_LIMIT;
416
+ const status = loop ? 'triage' : 'blocked';
417
+ run(db, 'UPDATE tasks SET status = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL, block_kind = ?, block_recurrences = ? WHERE id = ?', status, kind, recurrences, id);
418
+ const runId = endRun(db, id, { outcome: 'blocked', summary: reason }) ?? (reason ? synthesizeRun(db, id, { outcome: 'blocked', summary: reason }) : null);
419
+ event(db, id, loop ? 'block_loop_detected' : 'blocked', { reason, kind, recurrences, ...(loop ? { limit: BLOCK_RECURRENCE_LIMIT } : {}), source_status: source }, runId);
420
+ return status;
418
421
  }
419
422
 
420
423
  export function unblock(board, id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
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": {