@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.
@@ -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 close that failed for a reason another attempt cannot change is tried once and raised as an incident, not tried
1014
- // every round (each attempt is a process). A deferred close (an offline machine, a run that still owns the pane) waits.
1015
- if (session.close_failed_at && session.close_error && !/^close deferred/.test(session.close_error)) continue;
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 }, row.id);
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
- for (const { id } of running) { const session = currentRun(board.db, id)?.metadata?.supercode; await readBefore(fleet, session); if (session?.session_id) updatedAt(session); }
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 } : undefined);
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 harness holds (a question or permission prompt, a dialog such as the usage limit, a hang) is told to
1151
- // the manager with what its screen shows, once per state: a reminder typed into a dialog goes nowhere. Waiting on
1152
- // its user is known at once; working without writing past the quiet threshold is held.
1153
- const held = live?.turn === 'needs_input' ? 'waiting_on_user' : live?.turn === 'working' && lastAt > 0 && observedAt - lastAt >= settings.idleQuiet ? 'working_silent' : null;
1154
- if (held) {
1155
- if (session.idle_observation?.decision === held && session.idle_observation.last_message_at_ms === reading.last_message_at_ms) return;
1156
- // (its screen is not read: the call here named `panes.capture`, no machine door, and never answered)
1157
- const screen = null;
1158
- const observation = retain(held, { screen });
1159
- const why = held === 'waiting_on_user' ? 'is waiting on its user (a question or a permission prompt)' : `has reported working without writing anything for ${Math.round((observedAt - lastAt) / 60)} min`;
1160
- const text = `${id}: its session ${addressOf(session)} ${why}; nothing it is sent reaches it until that is answered.${screen ? `\nIts screen:\n${screen}` : ''}`;
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, held, addressOf(session)]);
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.
@@ -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.108",
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": {