@volter/supercode-orchestrator 0.5.108 → 0.5.110
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 +52 -21
- package/board/machine-fleet.mjs +3 -1
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -1010,9 +1010,15 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
|
|
|
1010
1010
|
(report.closed ??= []).push({ task_id: row.task_id, ...session, released: true });
|
|
1011
1011
|
continue;
|
|
1012
1012
|
}
|
|
1013
|
-
// A
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1013
|
+
// A final refusal stops further close attempts. Only an authorized observation of this exact pane may establish
|
|
1014
|
+
// absence: panes.list can be filtered or carry missing context keys. Unknown/invisible keeps the original failure.
|
|
1015
|
+
const finalFailure = Boolean(session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error));
|
|
1016
|
+
if (finalFailure) {
|
|
1017
|
+
if (!session.pane) continue;
|
|
1018
|
+
const r = await doorOn(session.machine ?? localMachine(), 'harness.v1.panes.presence', { pane: session.pane }, { timeout: 10_000, as: boardAddress(board.label) });
|
|
1019
|
+
const observed = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
1020
|
+
if (observed?.pane !== session.pane || observed.present !== false) continue;
|
|
1021
|
+
}
|
|
1016
1022
|
const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
|
|
1017
1023
|
// Persist retries even for historical ends that did not have a close_session action. The close is claimed in the
|
|
1018
1024
|
// same transaction, before the pane or process is touched: of two rounds (or a round and a verb) that read one close,
|
|
@@ -1039,7 +1045,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
|
|
|
1039
1045
|
if (row.ended_at) { const inTurn = await turnAtClose(session); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
|
|
1040
1046
|
await stopSession(board, session);
|
|
1041
1047
|
}
|
|
1042
|
-
} else {
|
|
1048
|
+
} else if (!finalFailure) {
|
|
1043
1049
|
if (!panes.has(machine)) {
|
|
1044
1050
|
const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
|
|
1045
1051
|
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
@@ -1088,7 +1094,8 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
|
|
|
1088
1094
|
const subscribed = db.prepare('SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)').all()
|
|
1089
1095
|
.some((r) => addressOf(json(r.metadata, {})?.supercode) === address && r.id !== row.id);
|
|
1090
1096
|
const unsubscribed = subscribed ? 0 : db.prepare("DELETE FROM kanban_notify_subs WHERE platform = 'supercode' AND chat_id = ? AND (task_id = ? OR json_extract(delivery_metadata, '$.role') = 'worker')").run(address, row.task_id).changes;
|
|
1091
|
-
event(db, row.task_id, 'session_closed', { session_id: session.session_id, pane: session.pane, machine: session.machine, unsubscribed
|
|
1097
|
+
event(db, row.task_id, 'session_closed', { session_id: session.session_id, pane: session.pane, machine: session.machine, unsubscribed,
|
|
1098
|
+
...(finalFailure ? { reason: 'authorized pane presence observed absence after an earlier close refusal' } : {}) }, row.id);
|
|
1092
1099
|
}
|
|
1093
1100
|
});
|
|
1094
1101
|
if (error) report.undelivered.push([row.task_id, addressOf(session), error, 'close']);
|
|
@@ -1110,7 +1117,12 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
1110
1117
|
};
|
|
1111
1118
|
const managers = [].concat(entry.params.managers ?? []).filter(Boolean);
|
|
1112
1119
|
const running = 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();
|
|
1113
|
-
|
|
1120
|
+
// every running card a session is on: a session waiting on its user is told once, naming them all
|
|
1121
|
+
const cardsOf = new Map();
|
|
1122
|
+
for (const { id } of running) {
|
|
1123
|
+
const session = currentRun(board.db, id)?.metadata?.supercode; await readBefore(fleet, session); if (session?.session_id) updatedAt(session);
|
|
1124
|
+
if (addressOf(session)) cardsOf.set(addressOf(session), [...(cardsOf.get(addressOf(session)) ?? []), id]);
|
|
1125
|
+
}
|
|
1114
1126
|
for (const { id } of running) {
|
|
1115
1127
|
let staged = [];
|
|
1116
1128
|
txRead(board, (db) => {
|
|
@@ -1125,7 +1137,8 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
1125
1137
|
// a session in no pane has its turn on its machine's discovery row, which sessionAlive just read
|
|
1126
1138
|
let nativeRow = null;
|
|
1127
1139
|
if (session.surface === 'native') try { nativeRow = nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id), session).row; } catch { /* unread: no turn */ }
|
|
1128
|
-
const live = fleet.sessions.find((x) => x.address === addressOf(session)) ?? (nativeRow ? { address: addressOf(session), turn: nativeRow.activity?.turn ?? null
|
|
1140
|
+
const live = fleet.sessions.find((x) => x.address === addressOf(session)) ?? (nativeRow ? { address: addressOf(session), turn: nativeRow.activity?.turn ?? null, waiting_for: nativeRow.activity?.evidence?.waiting_for ?? null,
|
|
1141
|
+
turn_source: nativeRow.activity?.evidence?.source ?? null, turn_since_ms: nativeRow.activity?.evidence?.native_state_since_ms ?? null } : undefined);
|
|
1129
1142
|
const observedAt = now();
|
|
1130
1143
|
const lastAtMs = Number(live?.last_message_at_ms) || updatedAt(session);
|
|
1131
1144
|
const lastAt = Math.floor(Number(lastAtMs) / 1000);
|
|
@@ -1139,7 +1152,7 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
1139
1152
|
const retain = (decision, extra = {}) => {
|
|
1140
1153
|
// A reading that decides nothing is kept once per state: rewriting it each pass would wake the next pass.
|
|
1141
1154
|
const prior = session.idle_observation;
|
|
1142
|
-
if (!['dispatched', 'dispatch_failed'].includes(decision) && prior?.decision === decision && prior.last_message_at_ms === reading.last_message_at_ms && prior.status === reading.status) return prior;
|
|
1155
|
+
if (!['dispatched', 'dispatch_failed', 'waiting_on_user'].includes(decision) && prior?.decision === decision && prior.last_message_at_ms === reading.last_message_at_ms && prior.status === reading.status) return prior;
|
|
1143
1156
|
const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
|
|
1144
1157
|
const patch = { idle_observation: observation };
|
|
1145
1158
|
if (decision === 'below_threshold') patch.idle_below_threshold = observation;
|
|
@@ -1147,21 +1160,39 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
1147
1160
|
setRunSession(db, runRow.id, patch);
|
|
1148
1161
|
return observation;
|
|
1149
1162
|
};
|
|
1150
|
-
// A session its
|
|
1151
|
-
// the manager with what
|
|
1152
|
-
// its user is
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1163
|
+
// A session waiting on its user (a question, a permission prompt) or working without writing past the quiet
|
|
1164
|
+
// threshold is told to the manager, with only what was read of it. Whether a harness's own state is a question
|
|
1165
|
+
// put to its user is its harness's to say (`needs_input`); what it waits for is carried as it was read.
|
|
1166
|
+
const address = addressOf(session);
|
|
1167
|
+
const asked = live?.turn === 'needs_input';
|
|
1168
|
+
// a reading that shows the session no longer waiting ends the prompt; a round that did not read it does not
|
|
1169
|
+
if (session.held_notice && live && !asked) setRunSession(db, runRow.id, { held_notice: null });
|
|
1170
|
+
if (asked) {
|
|
1171
|
+
// One prompt is one notice: the same wait (what it waits for, since when its harness says) on the same
|
|
1172
|
+
// session, whichever of its cards a round reads first and however long it stands.
|
|
1173
|
+
const key = [address, live.waiting_for ?? '', live.turn_since_ms ?? ''].join('|');
|
|
1174
|
+
if (session.held_notice?.key === key) return;
|
|
1175
|
+
// every run a card works under: an ended run can still be its card's current one
|
|
1176
|
+
const told = db.prepare('SELECT r.metadata FROM task_runs r WHERE r.id != ? AND (r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id))').all(runRow.id).some((r) => json(r.metadata, {})?.supercode?.held_notice?.key === key);
|
|
1177
|
+
setRunSession(db, runRow.id, { held_notice: { key, at: observedAt } });
|
|
1178
|
+
if (told) return;
|
|
1179
|
+
const observation = retain('waiting_on_user', { waiting_for: live.waiting_for ?? null, since_ms: live.turn_since_ms ?? null });
|
|
1180
|
+
const others = (cardsOf.get(address) ?? []).filter((card) => card !== id);
|
|
1181
|
+
const what = live.turn_source === 'pane_prompt' ? 'shows a prompt on its pane that waits for an answer'
|
|
1182
|
+
: `reports that it is waiting on its user${live.waiting_for ? ` (${live.waiting_for})` : ''}${live.turn_since_ms ? ` since ${new Date(Number(live.turn_since_ms)).toISOString().slice(0, 19)}Z` : ''}`;
|
|
1183
|
+
event(db, id, 'session_held', observation, runRow.id);
|
|
1184
|
+
const to = managers[0] ?? null;
|
|
1185
|
+
if (to) event(db, id, 'board_message', { to, text: `${id}: its session ${address} ${what}.${others.length ? ` It is also on ${others.join(', ')}.` : ''}` }, null);
|
|
1186
|
+
(report.held ??= []).push([id, 'waiting_on_user', address]);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (live?.turn === 'working' && lastAt > 0 && observedAt - lastAt >= settings.idleQuiet) {
|
|
1190
|
+
if (session.idle_observation?.decision === 'working_silent' && session.idle_observation.last_message_at_ms === reading.last_message_at_ms) return;
|
|
1191
|
+
const observation = retain('working_silent');
|
|
1161
1192
|
event(db, id, 'session_held', observation, runRow.id);
|
|
1162
1193
|
const to = managers[0] ?? null;
|
|
1163
|
-
if (to) event(db, id, 'board_message', { to, text }, null);
|
|
1164
|
-
(report.held ??= []).push([id,
|
|
1194
|
+
if (to) event(db, id, 'board_message', { to, text: `${id}: its session ${address} has reported working without writing anything for ${Math.round((observedAt - lastAt) / 60)} min.` }, null);
|
|
1195
|
+
(report.held ??= []).push([id, 'working_silent', address]);
|
|
1165
1196
|
return;
|
|
1166
1197
|
}
|
|
1167
1198
|
// No timestamp is no reading, and no reading gives no verdict.
|
package/board/machine-fleet.mjs
CHANGED
|
@@ -66,7 +66,9 @@ export async function readMachineFleet(machines, local) {
|
|
|
66
66
|
native.set(address, { alive: running ? true : null, states: [running ? true : null] });
|
|
67
67
|
if (running) alive.add(address);
|
|
68
68
|
sessions.push({ address, machine: row.machine, harness: activity.harness, session_id: activity.session_id, pane: pane.pane, name: pane.title ?? pane.label ?? null,
|
|
69
|
-
state: running ? 'running' : 'unknown', turn: activity.turn ?? null, native_state: activity.evidence?.native_state ?? null
|
|
69
|
+
state: running ? 'running' : 'unknown', turn: activity.turn ?? null, native_state: activity.evidence?.native_state ?? null,
|
|
70
|
+
// what a session that needs input says it waits for and since when, where its harness publishes them
|
|
71
|
+
waiting_for: activity.evidence?.waiting_for ?? null, turn_source: activity.evidence?.source ?? null, turn_since_ms: activity.evidence?.native_state_since_ms ?? null });
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
const ok = online.has(local);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.110",
|
|
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": {
|