@volter/supercode-orchestrator 0.5.109 → 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.
@@ -1117,7 +1117,12 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
1117
1117
  };
1118
1118
  const managers = [].concat(entry.params.managers ?? []).filter(Boolean);
1119
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();
1120
- 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
+ }
1121
1126
  for (const { id } of running) {
1122
1127
  let staged = [];
1123
1128
  txRead(board, (db) => {
@@ -1132,7 +1137,8 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
1132
1137
  // a session in no pane has its turn on its machine's discovery row, which sessionAlive just read
1133
1138
  let nativeRow = null;
1134
1139
  if (session.surface === 'native') try { nativeRow = nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id), session).row; } catch { /* unread: no turn */ }
1135
- 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);
1136
1142
  const observedAt = now();
1137
1143
  const lastAtMs = Number(live?.last_message_at_ms) || updatedAt(session);
1138
1144
  const lastAt = Math.floor(Number(lastAtMs) / 1000);
@@ -1146,7 +1152,7 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
1146
1152
  const retain = (decision, extra = {}) => {
1147
1153
  // A reading that decides nothing is kept once per state: rewriting it each pass would wake the next pass.
1148
1154
  const prior = session.idle_observation;
1149
- 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;
1150
1156
  const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
1151
1157
  const patch = { idle_observation: observation };
1152
1158
  if (decision === 'below_threshold') patch.idle_below_threshold = observation;
@@ -1154,21 +1160,39 @@ async function observeIdle(board, fleet, settings, report, entry, effects) {
1154
1160
  setRunSession(db, runRow.id, patch);
1155
1161
  return observation;
1156
1162
  };
1157
- // A session its harness holds (a question or permission prompt, a dialog such as the usage limit, a hang) is told to
1158
- // the manager with what its screen shows, once per state: a reminder typed into a dialog goes nowhere. Waiting on
1159
- // its user is known at once; working without writing past the quiet threshold is held.
1160
- const held = live?.turn === 'needs_input' ? 'waiting_on_user' : live?.turn === 'working' && lastAt > 0 && observedAt - lastAt >= settings.idleQuiet ? 'working_silent' : null;
1161
- if (held) {
1162
- if (session.idle_observation?.decision === held && session.idle_observation.last_message_at_ms === reading.last_message_at_ms) return;
1163
- // (its screen is not read: the call here named `panes.capture`, no machine door, and never answered)
1164
- const screen = null;
1165
- const observation = retain(held, { screen });
1166
- 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`;
1167
- 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');
1168
1192
  event(db, id, 'session_held', observation, runRow.id);
1169
1193
  const to = managers[0] ?? null;
1170
- if (to) event(db, id, 'board_message', { to, text }, null);
1171
- (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]);
1172
1196
  return;
1173
1197
  }
1174
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.109",
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": {