@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.
- package/board/dispatch.mjs +17 -18
- package/board/engine.mjs +20 -17
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -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,
|
|
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.
|
|
919
|
-
*
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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,
|
|
416
|
-
return
|
|
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.
|
|
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": {
|