@volter/supercode-orchestrator 0.5.37 → 0.5.38

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.
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // One fleet reading for a synchronous board process: each named machine's panes and the harness session under each live
3
+ // pane, every machine read at once through its own door, each bounded by its own deadline. Prints one row per machine:
4
+ // { machine, panes, activities } when its door answered, { machine, error, code } when it did not.
5
+ import { openMachine } from '@volter/teams/machine-door';
6
+ import { StreamRpc, harnessTransport } from '@volter/teams/client';
7
+ import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
8
+ let input = ''; for await (const chunk of process.stdin) input += chunk;
9
+ const { machines, deadlineMs } = JSON.parse(input);
10
+ const within = (promise, ms) => Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(Object.assign(new Error(`no answer within ${ms} ms`), { code: 'timeout' })), ms))]);
11
+ async function read({ name, at }) {
12
+ let channel;
13
+ try {
14
+ channel = await openMachine({ machine: at });
15
+ const panes = await new StreamRpc(await channel.open('node')).call('harness.v1.panes.list', {});
16
+ const live = (Array.isArray(panes) ? panes : []).filter((pane) => !pane.dead && pane.pid);
17
+ let activities = [];
18
+ if (live.length) {
19
+ const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
20
+ try { await client.start?.(); activities = (await client.request('harness.v1.sessions.activity_under', { pids: live.map((pane) => pane.pid) }))?.activities ?? []; }
21
+ finally { await client.close?.(); }
22
+ }
23
+ return { machine: name, panes, activities };
24
+ } finally { channel?.close(); }
25
+ }
26
+ const rows = await Promise.all(machines.map((machine) => within(read(machine), deadlineMs)
27
+ .catch((error) => ({ machine: machine.name, error: String(error?.message ?? error).slice(0, 300), code: error?.code ?? 'unavailable' }))));
28
+ process.stdout.write(`${JSON.stringify(rows)}\n`);
29
+ process.exit(0);
package/board/cli.mjs CHANGED
@@ -1440,7 +1440,7 @@ async function serve(root, args = {}) {
1440
1440
  const running = installed();
1441
1441
  log(`dispatching the boards of ${root} from ${localMachine()} (orchestrator ${running ?? 'unknown'})`);
1442
1442
  events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log });
1443
- log('dispatch wakes on board writes, document edits, board mail and session catalogue events; interval is a safety sweep');
1443
+ log('dispatch wakes on board writes, document edits and board mail; the interval sweep reads each machine\'s own door');
1444
1444
  const stuck = {};
1445
1445
  const warned = {};
1446
1446
  let pausedSaid = false;
@@ -16,11 +16,11 @@
16
16
  // subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
17
17
  // launch and a crash between the two steps is answered by the next tick.
18
18
  import { monitorsRuns, failedObservation, surfaceIncident, auditEndedRuns, originOf } from './incidents.mjs';
19
- import { readEventFleet } from './event-fleet.mjs';
20
- let eventFleetRoot = null;
19
+ import { readMachineFleet } from './machine-fleet.mjs';
20
+ let fleetRoot = null;
21
21
  import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
22
22
  import { runtimeDoor } from './runtime-door.mjs';
23
- import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
23
+ import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
24
24
  import { materializeAgentHome } from '../agent-home.mjs';
25
25
  import { memorySnapshotSync } from '../hermes-cli.mjs';
26
26
  import { createRequire } from 'node:module';
@@ -115,9 +115,22 @@ export function callerSession() {
115
115
  return claude;
116
116
  }
117
117
 
118
- export function observeFleet(root = eventFleetRoot) {
119
- const fleet = readEventFleet(root);
120
- presenceReading = { known: fleet.presenceKnown === true, online: new Set([localMachine(), ...fleet.online]) };
118
+ /** The fleet as the home's machines answer for themselves (machine-fleet.mjs): this machine, and every machine an open
119
+ * card of the home names or one of its notice subscribers is on. */
120
+ export function observeFleet(root = fleetRoot) {
121
+ const machines = new Set();
122
+ for (const slug of root ? listBoards(root) : []) {
123
+ try {
124
+ const b = openBoard(root, slug);
125
+ try {
126
+ const live = "t.status NOT IN ('done', 'cancelled', 'archived') AND NOT COALESCE(c.archived, 0)";
127
+ for (const { machine } of b.db.prepare(`SELECT DISTINCT c.machine FROM supercode_cards c JOIN tasks t ON t.id = c.task_id WHERE c.machine IS NOT NULL AND c.machine != '' AND ${live}`).all()) machines.add(normalMachine(machine));
128
+ for (const { chat_id: address } of b.db.prepare(`SELECT DISTINCT s.chat_id FROM kanban_notify_subs s JOIN tasks t ON t.id = s.task_id JOIN supercode_cards c ON c.task_id = t.id WHERE s.platform = 'supercode' AND s.chat_id LIKE 'sc:%' AND ${live}`).all()) machines.add(normalMachine(address.split(':')[1]));
129
+ } finally { b.close(); }
130
+ } catch { /* its own tick reports it */ }
131
+ }
132
+ const fleet = readMachineFleet(machines, localMachine());
133
+ presenceReading = { known: fleet.presenceKnown === true, online: new Set([localMachine(), ...fleet.online]), unknown: fleet.unknown ?? new Set() };
121
134
  notePresence(presenceReading);
122
135
  return fleet;
123
136
  }
@@ -139,24 +152,27 @@ const adoptedOnly = (s) => s?.mode === 'adopt' && !s.launch_key && s.surface !==
139
152
  const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
140
153
  const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
141
154
 
142
- // A run Teams never published (one outside a synced root, a scratch workspace's) is read on its own machine: a
143
- // headless worker by its pid, a pane by that machine's pane list. Null only when the machine cannot answer.
155
+ // A session in a live pane of its machine is the fleet's reading; any other run is read on its own machine: a headless
156
+ // worker by its pid, a pane by that machine's pane list, a session in no pane (an adopted native one) by that machine's
157
+ // harness discovery. Null only when the machine cannot answer.
144
158
  const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : unpublishedAlive(session);
159
+ /** A subscriber's address as the session it names, for sessionAlive (its machine, harness and native id). */
160
+ const sessionAt = (address) => { const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(address)); return m && HARNESSES[m[2]] ? { address, machine: normalMachine(m[1]), harness: m[2], session_id: m[3] } : null; };
161
+ const subscriberAlive = (fleet, address) => fleet.alive.has(address) || (sessionAt(address) ? sessionAlive(fleet, sessionAt(address)) === true : false);
145
162
  const paneLists = new Map();
146
- // A run the agent sync holds names only its machine and native session (`harness:id`), never a pane or pid. Teams'
147
- // fleet answers for a session it published; one it never published (a gateway worker in its profile's directory,
148
- // outside every synced root) is read through its machine's native discovery, as a native close reads it.
163
+ // A run the agent sync holds names only its machine and native session (`harness:id`), never a pane or pid.
149
164
  function syncRowAlive(fleet, row) {
150
165
  const machine = row.machine || localMachine(), at = row.session_ref.indexOf(':');
151
- const session = { machine, harness: row.session_ref.slice(0, at), session_id: row.session_ref.slice(at + 1), address: `sc:${machine}:${row.session_ref}` };
152
- const alive = sessionAlive(fleet, session);
153
- if (alive !== null) return alive;
154
- try { return nativeReading(nativeSessions(machine, session.harness), session).alive; } catch { return null; }
166
+ return sessionAlive(fleet, { machine, harness: row.session_ref.slice(0, at), session_id: row.session_ref.slice(at + 1), address: `sc:${machine}:${row.session_ref}` });
155
167
  }
156
168
  function unpublishedAlive(session) {
157
169
  if (!session) return null;
158
170
  if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
159
- if (!session.pane || !session.machine) return null;
171
+ if (!session.machine) return null;
172
+ if (!session.pane) {
173
+ if (!session.session_id) return null;
174
+ try { return nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code'), session).alive; } catch { return null; }
175
+ }
160
176
  let entry = paneLists.get(session.machine);
161
177
  if (!entry || Date.now() - entry.at > 5000) {
162
178
  const r = doorOn(session.machine, 'harness.v1.panes.list', {}, { timeout: 60_000 });
@@ -177,12 +193,14 @@ function nativeSessions(machine, harness) {
177
193
  discoveries.set(key, { at: Date.now(), sessions });
178
194
  return sessions;
179
195
  }
196
+ // A native discovery is bounded as the fleet reading is: one that does not answer in time is unknown, read next round.
197
+ const NATIVE_DISCOVERY_MS = 15_000;
180
198
  function discoverNative(machine, harness) {
181
199
  if (offlineMachine(machine)) throw unavailable(`native liveness unavailable: machine ${normalMachine(machine)} is offline`);
182
200
  // This machine's discovery is its harness door, as every other harness read here is; another machine's is supercode's fleet arm
183
201
  const result = normalMachine(machine) === localMachine()
184
- ? doorOn(machine, 'harness.v1.sessions.discover', { harnesses: [harness], limit: 2000 }, { timeout: 120_000 })
185
- : sc(['discover', '--on', machine, '--harness', harness, '--limit', '2000'], { timeout: 120_000 });
202
+ ? doorOn(machine, 'harness.v1.sessions.discover', { harnesses: [harness], limit: 2000 }, { timeout: NATIVE_DISCOVERY_MS })
203
+ : sc(['discover', '--on', machine, '--harness', harness, '--limit', '2000'], { timeout: NATIVE_DISCOVERY_MS });
186
204
  const page = result.code === 0 ? lastJson(result.stdout) : null;
187
205
  if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
188
206
  return page.sessions;
@@ -256,8 +274,9 @@ export function lanesOf(profiles = {}) {
256
274
  return out;
257
275
  }
258
276
 
259
- // The latest presence reading (observeFleet). A machine it reads as offline is not asked: each remote call would
260
- // wait out its timeout, and one offline machine held every close and launch on this one.
277
+ // The latest presence reading (observeFleet). A machine that did not answer it (offline, or unread this round) is not
278
+ // asked again until the next reading: each remote call would wait out its timeout, and one silent machine held every
279
+ // close and launch on this one. Its door answers `machine_offline`, which every caller tries again next round.
261
280
  let presenceReading = null;
262
281
  const offlineMachine = (machine) => {
263
282
  const m = normalMachine(machine ?? localMachine());
@@ -265,7 +284,7 @@ const offlineMachine = (machine) => {
265
284
  };
266
285
  /** A machine's public door (`harness.v1.*`), answered at once when the machine reads as offline. */
267
286
  const doorOn = (machine, method, params, options) => offlineMachine(machine)
268
- ? { code: 1, stdout: '', stderr: `machine ${normalMachine(machine)} is offline` }
287
+ ? { code: 1, stdout: '', stderr: JSON.stringify({ code: 'machine_offline', message: `machine ${normalMachine(machine)} did not answer the fleet reading` }) }
269
288
  : runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
270
289
  const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
271
290
 
@@ -329,11 +348,11 @@ function supervise(board, fleet, settings, report, entry, effects) {
329
348
  } else extendClaim(db, id, { ttl: settings.ttl, reason: 'launch_receipt_pending' });
330
349
  return;
331
350
  }
332
- // Availability is the same retained linked/unlinked feed subscribers hear.
333
- // A failed stream is unknown, not evidence that every remote machine left.
351
+ // Availability is the machine's own door (observeFleet). A machine whose door did not answer is unknown, not
352
+ // evidence that it left: its run is kept. Only a relay's positive answer that it is not linked reads as offline.
334
353
  if (session?.session_id && session.surface !== 'headless') {
335
354
  const reachable = fleet.online.has(session.machine);
336
- if (!reachable && !fleet.presenceKnown) return;
355
+ if (!reachable && (!fleet.presenceKnown || fleet.unknown?.has(session.machine))) return;
337
356
  if (!reachable) {
338
357
  const next = {...session, offline_since: session.offline_since ?? t};
339
358
  setRunSession(db,runRow.id,next);
@@ -736,7 +755,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
736
755
  export async function startCard(board, id, { roles = ['manager'], lanes, settings } = {}) {
737
756
  // the board's own published fleet projection, as a tick reads it: a manager's or creator's start that continues the
738
757
  // slot's session checks that session through it
739
- eventFleetRoot = board.root;
758
+ fleetRoot = board.root;
740
759
  const entry = await loadWorkflow(board.root);
741
760
  const fleet = observeFleet();
742
761
  const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
@@ -933,8 +952,11 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
933
952
  const runRow = currentRun(db, id);
934
953
  const session = runRow?.metadata?.supercode;
935
954
  if (!session?.session_id || !['pane', 'native'].includes(session.surface) || session.offline_since != null) return;
936
- if (session.surface === 'native' && fleet.native?.get(addressOf(session))?.alive !== true) return;
937
- const live = fleet.sessions.find((x) => x.address === addressOf(session));
955
+ if (session.surface === 'native' && sessionAlive(fleet, session) !== true) return;
956
+ // a session in no pane has its turn on its machine's discovery row, which sessionAlive just read
957
+ let nativeRow = null;
958
+ if (session.surface === 'native') try { nativeRow = nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code'), session).row; } catch { /* unread: no turn */ }
959
+ const live = fleet.sessions.find((x) => x.address === addressOf(session)) ?? (nativeRow ? { address: addressOf(session), turn: nativeRow.activity?.turn ?? null } : undefined);
938
960
  const observedAt = now();
939
961
  const lastAtMs = Number(live?.last_message_at_ms) || updatedAt(session);
940
962
  const lastAt = Math.floor(Number(lastAtMs) / 1000);
@@ -989,7 +1011,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
989
1011
  const observation = retain('dispatched', { turns_after: Number(stored?.goal_turns ?? 0), nudge_queued: r.effects.some(e => e.kind === 'send_to_actor'), run_ended: after?.id !== runRow.id });
990
1012
  event(db, id, 'session_idle_observed', observation, runRow.id);
991
1013
  if (after?.id === runRow.id) setRunSession(db, runRow.id, { goal_nudged_at: observedAt });
992
- report.notified.push([id, 'session_idle', live.address]);
1014
+ report.notified.push([id, 'session_idle', addressOf(session)]);
993
1015
  } catch (error) {
994
1016
  retain('dispatch_failed', { error: String(error.message).slice(-200) });
995
1017
  report.failed.push([id, `session_idle: ${String(error.message).slice(-200)}`]);
@@ -1121,7 +1143,7 @@ function launcherCheck(db, task, lane, fleet) {
1121
1143
  if (!task.machine) return 'the card names no machine; a pane session runs on the machine its card names';
1122
1144
  const machine = normalMachine(task.machine);
1123
1145
  if (!fleet.ok) return `supercode could not read the fleet: ${fleet.error}`;
1124
- if (machine !== localMachine() && !fleet.presenceKnown) return 'machine event stream unavailable; launch waits for a current availability reading';
1146
+ if (fleet.unknown?.has(machine)) return `machine ${machine}'s door did not answer; the card starts when it does`;
1125
1147
  if (!fleet.online.has(machine)) return `machine ${machine} is offline; the card starts when it is back`;
1126
1148
  // a card on this machine gets its workspace made here (scratch, dir or worktree, as Hermes makes it); another
1127
1149
  // machine's card names its directory there
@@ -1266,7 +1288,7 @@ function bindLaunch(board, row, session, pane, sid) {
1266
1288
 
1267
1289
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
1268
1290
  export function reconcileCallerLaunch(board, sid) {
1269
- eventFleetRoot = board.root;
1291
+ fleetRoot = board.root;
1270
1292
  if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
1271
1293
  WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
1272
1294
  const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
@@ -1299,15 +1321,6 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1299
1321
  for(const row of agentRuns){
1300
1322
  const session=json(row.metadata,{})?.supercode;
1301
1323
  if(!session?.session_id||!session.agent_org)continue;
1302
- const observation=fleet.sessions?.find(item=>item.address===addressOf(session))?.identity;
1303
- if(observation?.agent?.id===session.agent_id&&session.agent_profile_revision!==observation.revision){
1304
- const mismatch=agentProfileMismatch(session.agent_profile_intent,observation.resolved);
1305
- tx(board,db=>{
1306
- // Rule 4: a run whose recorded profile differs from its agent's intent is shown and not counted.
1307
- setRunSession(db,row.id,{agent_profile_recorded:observation.resolved,agent_profile_revision:observation.revision,agent_profile_mismatch:mismatch,counted:!mismatch.differences.length});
1308
- event(db,row.task_id,mismatch.differences.length?'agent_profile_drift':'agent_profile_recorded',{agent:session.agent_id,session:session.session_id,comparison:mismatch},row.id);
1309
- });
1310
- }
1311
1324
  const ref=`${session.harness}:${session.session_id}`;
1312
1325
  // An ended run whose session the agent store already holds, published, for another agent lost that claim when it
1313
1326
  // ran: no round can change an ended run or a published owner, so the disagreement is history. It is said once on
@@ -1327,7 +1340,8 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1327
1340
  orgs.set(session.agent_org,session.agent_context);
1328
1341
  }
1329
1342
  // Identities minted while Teams was absent are declared under their ids before their sessions are joined.
1330
- const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context);
1343
+ const context=process.env.SUPERCODE_CONTEXT;
1344
+ let scope;try{scope=agentScope(context);}catch(error){scope={offline:true};report.failed.push(['agents',`agent declarations wait: ${error.message}`]);}
1331
1345
  if(!scope.offline){
1332
1346
  orgs.set(scope.org,orgs.get(scope.org)??context);
1333
1347
  for(const pending of synchronizeLocalAgents({root:board.root,profiles:entry.profiles,context}))report.queued.push([pending.agent,`agent declaration pending: ${pending.error}`]);
@@ -1436,7 +1450,8 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1436
1450
  if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
1437
1451
  const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
1438
1452
  const how = HARNESSES[harness];
1439
- const base = ['open', '--new', how.program, '--detach', `--window=${window}`, '--on', task.machine, '--cwd', task.workspace_path, '--env', 'SUPERCODE_BOARD_PROCESS_GUARD=1',
1453
+ // A card on this machine opens through this machine's own daemon; only another machine is reached with `--on`.
1454
+ const base = ['open', '--new', how.program, '--detach', `--window=${window}`, ...(machine === localMachine() ? [] : ['--on', task.machine]), '--cwd', task.workspace_path, '--env', 'SUPERCODE_BOARD_PROCESS_GUARD=1',
1440
1455
  ...Object.entries(meta.agent_env??{}).flatMap(([key,value])=>['--env',`${key}=${value}`])];
1441
1456
  // the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
1442
1457
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
@@ -1497,10 +1512,14 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1497
1512
  };
1498
1513
  let session = null;
1499
1514
  let replaced = null;
1500
- if (prev?.surface === 'native' && !prev.pane && !prev.close_confirmed_at) {
1515
+ // A session in no pane is read on its machine before anything replaces it. One the board released (an adopted
1516
+ // session it did not launch, left running at its run's end) is its owner's while it runs: it is never resumed beside
1517
+ // itself, and the launch waits for the owner to end it.
1518
+ if (prev?.surface === 'native' && !prev.pane && (!prev.close_confirmed_at || prev.released)) {
1501
1519
  const reading = nativeReading(nativeSessions(prev.machine, prev.harness), prev);
1502
1520
  if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1503
1521
  fleet.native ??= new Map(); fleet.native.set(addressOf(prev), reading);
1522
+ if (reading.alive && prev.released) throw unavailable(`released session ${addressOf(prev)} still runs; the launch waits for its owner to end it`);
1504
1523
  if (reading.alive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw unavailable(`native session ${addressOf(prev)} is still live; its close must finish before replacement`);
1505
1524
  }
1506
1525
  if (prev?.forgotten) {
@@ -1750,25 +1769,13 @@ function adoptionOpenTo(db, id, request, open) {
1750
1769
  return Boolean(claim) && claim.request === request && claim.lock === claimer();
1751
1770
  }
1752
1771
 
1753
- /**
1754
- * A session on this machine that Teams never published (one outside every synced root: a profile's own directory) is
1755
- * found through this machine's own doors: its panes, and the native session each pane's process runs. Only a full
1756
- * address is looked up here; liveness is still verified below as for any other session.
1757
- */
1758
- function localListing(who) {
1772
+ /** A session in no pane (a native process its machine's harness discovery holds as running), by its full address. */
1773
+ function nativeListing(who) {
1759
1774
  const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(who));
1760
- if (!m || normalMachine(m[1]) !== localMachine()) return null;
1761
- const [, , harness, sessionId] = m;
1762
- const listed = runtimeDoor(null, 'harness.v1.panes.list', {}, { timeout: 60_000 });
1763
- const panes = (listed.code === 0 ? json(listed.stdout.trim(), []) : []) ?? [];
1764
- const live = panes.filter((p) => !p.dead && p.pid);
1765
- if (!live.length) return null;
1766
- const r = runtimeDoor(null, 'harness.v1.sessions.activity_under', { pids: live.map((p) => p.pid) }, { timeout: 30_000 });
1767
- const row = (r.code === 0 ? json(r.stdout.trim(), {})?.activities ?? [] : []).find((a) => a.activity?.session_id === sessionId && (a.activity?.harness ?? harness) === harness);
1768
- // Not found in a pane of this machine: not listed, so a session opened moments ago is looked for again next tick.
1769
- const pane = row ? live.find((p) => p.pid === row.pid)?.pane ?? null : null;
1770
- if (!pane) return null;
1771
- return { address: `sc:${localMachine()}:${harness}:${sessionId}`, machine: localMachine(), harness, session_id: sessionId, pane, name: null };
1775
+ if (!m || !HARNESSES[m[2]]) return null;
1776
+ const [, host, harness, session_id] = m;
1777
+ try { return nativeReading(nativeSessions(host, harness), { session_id, harness }).alive === true ? { address: who, machine: normalMachine(host), harness, session_id, pane: null, name: null } : null; }
1778
+ catch { return null; }
1772
1779
  }
1773
1780
 
1774
1781
  /** The verb an adoption with no current run fires: the workflow's own `adopt` where it declares one, else its dispatch start. */
@@ -1790,7 +1797,7 @@ function adoptedRole(db, board, entry, task, run) {
1790
1797
  function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1791
1798
  fleet ??= observeFleet();
1792
1799
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1793
- const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? localListing(who);
1800
+ const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? nativeListing(who);
1794
1801
  if (!found) throw Object.assign(new Error(`the fleet lists no live session ${who}`), { notListed: true });
1795
1802
  const [, host, harness, sessionId] = found.address.split(':');
1796
1803
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
@@ -2105,7 +2112,7 @@ function endWorkerSubscriptions(board, fleet, report, natively = new Map()) {
2105
2112
  const ended = new Map();
2106
2113
  for (const row of db.prepare("SELECT task_id, chat_id, thread_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all()) {
2107
2114
  const address = row.chat_id, record = latest.get(address);
2108
- if (!record || record.session.released || held.has(address) || (fleet.ok && fleet.alive.has(address))) continue;
2115
+ if (!record || record.session.released || held.has(address) || (fleet.ok && subscriberAlive(fleet, address))) continue;
2109
2116
  const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at } : natively.get(address);
2110
2117
  if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
2111
2118
  if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
@@ -2132,7 +2139,7 @@ async function notify(board, fleet, report, entry) {
2132
2139
  // a session the fleet does not list this tick is told next tick: one reading is not a loss (a loaded box drops
2133
2140
  // sessions from a read, and a dropped subscription never came back), and a session that is replaced on its card
2134
2141
  // has its subscription moved when the new one starts
2135
- if (role !== 'creator' && fleet.ok && !fleet.alive.has(sub.chat_id)) return;
2142
+ if (role !== 'creator' && fleet.ok && !subscriberAlive(fleet, sub.chat_id)) return;
2136
2143
  const kinds = heardBy(entry, role);
2137
2144
  if (!kinds.length) return;
2138
2145
  const events = board.db.prepare(`SELECT id, kind, payload, created_at, run_id FROM task_events WHERE task_id = ? AND id > ? AND kind IN (${kinds.map(() => '?').join(', ')}) ORDER BY id`)
@@ -2459,7 +2466,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
2459
2466
 
2460
2467
  /** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
2461
2468
  export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
2462
- eventFleetRoot = root;
2469
+ fleetRoot = root;
2463
2470
  const loaded = await loadWorkflow(root, { fresh: true });
2464
2471
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
2465
2472
  const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
@@ -2569,10 +2576,10 @@ function capMax(cap, group, entry, lanes) {
2569
2576
  * effects in `runEffects` (send_to_actor, session_notice, merge_reviewed, declare_card_agent), board mail,
2570
2577
  * subscription notices (a doubled notice_delivered has been seen live), deferred-launch retries (two panes),
2571
2578
  * launch-receipt work (cancelUnissuedLaunch, bindLaunch), the Teams agent-run sync in reconcileLaunches, and
2572
- * board-only writes that do not re-check (a doubled agent_profile_recorded). Answers one report per board.
2579
+ * board-only writes that do not re-check. Answers one report per board.
2573
2580
  */
2574
2581
  export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
2575
- eventFleetRoot = root;
2582
+ fleetRoot = root;
2576
2583
  // a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
2577
2584
  const deployment = deploymentReceipt ? workflowRevision(root) : null;
2578
2585
  const loaded = await loadWorkflow(root, { fresh: true });
package/board/engine.mjs CHANGED
@@ -901,7 +901,7 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
901
901
  }
902
902
  if (task.status === 'running' && fleet?.ok) {
903
903
  const session = lastSession(db, task.id);
904
- if (session?.machine && !fleet.online.has(session.machine)) out.push({ kind: 'machine_offline', severity: 'warning', title: `its machine ${session.machine} is offline`, data: { machine: session.machine }, actions: [] });
904
+ if (session?.machine && !fleet.online.has(session.machine) && !fleet.unknown?.has(session.machine)) out.push({ kind: 'machine_offline', severity: 'warning', title: `its machine ${session.machine} is offline`, data: { machine: session.machine }, actions: [] });
905
905
  }
906
906
  const order = ['critical', 'error', 'warning'];
907
907
  return out.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity));
@@ -0,0 +1,49 @@
1
+ // The fleet as each machine the board works on answers for itself: its panes (`harness.v1.panes.list`) and the harness
2
+ // session beneath each live pane's process (`harness.v1.sessions.activity_under`). This is the reading the machine's
3
+ // connector publishes to Teams, taken at its source. Teams' session feed is for viewers; dispatch never waits on it
4
+ // (D137, D138).
5
+ //
6
+ // Every machine is read at once, each within DEADLINE_MS (bin/machine-fleet.mjs), so one hung machine or relay costs a
7
+ // reading that long, not the sum of them. A machine that answered is `online`. A remote machine that did not is
8
+ // `unknown` (its runs are kept, as an unread machine's always were), unless the relay positively answered that it is
9
+ // not linked (`machine_offline`): then it is offline. This machine's door not answering is no reading at all.
10
+ import { spawnSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const entry = fileURLToPath(new URL('../bin/machine-fleet.mjs', import.meta.url));
14
+ const DEADLINE_MS = 10_000;
15
+
16
+ /** One reading of `machines` (normalized names) and this machine, `local`. */
17
+ export function readMachineFleet(machines, local) {
18
+ const names = [...new Set([local, ...machines])];
19
+ const reply = spawnSync(process.execPath, [entry], { input: JSON.stringify({ machines: names.map((name) => ({ name, at: name === local ? null : name })), deadlineMs: DEADLINE_MS }),
20
+ encoding: 'utf8', timeout: DEADLINE_MS + 5_000, maxBuffer: 64 * 1024 * 1024 });
21
+ let rows = null;
22
+ try { rows = JSON.parse(String(reply.stdout).trim().split('\n').pop()); } catch { /* no reading */ }
23
+ const online = new Set(), unknown = new Set(), alive = new Set(), native = new Map(), sessions = [];
24
+ if (!Array.isArray(rows)) {
25
+ const error = String(reply.stderr || reply.error?.message || 'no answer').trim().slice(-300);
26
+ return { ok: false, presenceKnown: false, online, unknown: new Set(names), alive, native, sessions, error: `the fleet reading did not answer: ${error}` };
27
+ }
28
+ let localError = null;
29
+ for (const row of rows) {
30
+ if (row.error) {
31
+ if (row.machine === local) localError = row.error;
32
+ else if (row.code !== 'machine_offline') unknown.add(row.machine);
33
+ continue;
34
+ }
35
+ online.add(row.machine);
36
+ const live = (row.panes ?? []).filter((pane) => !pane.dead && pane.pid);
37
+ for (const { pid, activity } of row.activities ?? []) {
38
+ const pane = live.find((item) => item.pid === pid);
39
+ if (!pane || !activity?.session_id || !activity.harness) continue;
40
+ const address = `sc:${row.machine}:${activity.harness}:${activity.session_id}`, running = activity.presence === 'running';
41
+ native.set(address, { alive: running ? true : null, states: [running ? true : null] });
42
+ if (running) alive.add(address);
43
+ sessions.push({ address, machine: row.machine, harness: activity.harness, session_id: activity.session_id, pane: pane.pane, name: pane.title ?? pane.label ?? null,
44
+ state: running ? 'running' : 'unknown', turn: activity.turn ?? null, native_state: activity.evidence?.native_state ?? null });
45
+ }
46
+ }
47
+ const ok = online.has(local);
48
+ return { ok, presenceKnown: ok, online, unknown, alive, native, sessions, error: ok ? null : `machine ${local}'s door did not answer: ${localError ?? 'no row'}` };
49
+ }
package/board/wake.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  import { watch, existsSync, readFileSync, readdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
4
4
  import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { randomUUID } from 'node:crypto';
6
- import { followFleet } from './event-fleet.mjs';
7
6
  import { boardEventRelay } from './event-relay.mjs';
8
7
  import { fileURLToPath } from 'node:url';
9
8
 
@@ -27,7 +26,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
27
26
  if (!pending) pending = { at, sources: new Set() };
28
27
  pending.at = Math.min(pending.at, at);
29
28
  pending.sources.add(source);
30
- if (!source.startsWith('session-events')) void relay?.wake();
29
+ void relay?.wake();
31
30
  waiter?.();
32
31
  };
33
32
  const directory = (path, key, accept) => {
@@ -94,7 +93,6 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
94
93
  refresh();
95
94
  const supercodeHome = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME,'supercode') : join(env.HOME ?? '', '.config','supercode'));
96
95
  relay = boardEventRelay({root,supercodeHome,log});
97
- const fleet = followFleet({root,supercodeHome,wake,log});
98
96
  void relay.wake();
99
97
  return {
100
98
  refresh,
@@ -111,7 +109,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
111
109
  return result ? { at: result.at, sources: [...result.sources] } : null;
112
110
  },
113
111
  close() {
114
- closed = true; waiter?.(); fleet.close(); relay.close();
112
+ closed = true; waiter?.(); relay.close();
115
113
  for (const handle of watchers.values()) handle.close();
116
114
  watchers.clear();
117
115
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.37",
3
+ "version": "0.5.38",
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": {
@@ -1,66 +0,0 @@
1
- // A projection of retained Teams events. Dispatch reads this projection, never a periodically fetched catalogue.
2
- import { readFileSync, writeFileSync, renameSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { randomUUID } from 'node:crypto';
5
- import { eventClient, retryAfterMs } from '@volter/teams/event-client';
6
- const pathOf=root=>join(root,'.session-events.json');
7
- const normalize=value=>String(value??'').toLowerCase().replace(/[^a-z0-9-]/g,'-').replace(/-+/g,'-').replace(/^-|-$/g,'');
8
- const wait=(signal,ms)=>new Promise(resolve=>{const timer=setTimeout(resolve,ms);signal.addEventListener('abort',()=>{clearTimeout(timer);resolve();},{once:true});});
9
-
10
- export function followFleet({root,context,supercodeHome,wake,log}){
11
- const abort=new AbortController();let resources={},cursor=null,connected=false,snapshot=null;
12
- const persist=()=>{
13
- const path=pathOf(root),temporary=`${path}.${randomUUID()}.tmp`;
14
- writeFileSync(temporary,JSON.stringify({pid:process.pid,connected,cursor,resources}),{mode:0o600});renameSync(temporary,path);
15
- };
16
- const apply=(target,frame)=>{
17
- const event=frame.data;if(!event||!['session','machine'].includes(event.resource?.kind))return false;
18
- const key=`${event.resource.kind}:${event.resource.id}`,old=target[key];
19
- if(old&&old.sequence>=frame.sequence)return false;
20
- target[key]={sequence:frame.sequence,data:event.data};return true;
21
- };
22
- void(async()=>{
23
- // Refused for load, the follower waits out the server's retry_after, doubling while it is refused (to 5 min), and
24
- // keeps its cursor: asking every 5 s spent the slot it waited for on the next refusal.
25
- let refused=0;
26
- while(!abort.signal.aborted){
27
- let pause=5000;
28
- try{
29
- const {team}=eventClient({context,supercodeHome});
30
- // Every connection begins with a complete, ACL-filtered snapshot. A process restart cannot trust an old cache.
31
- for await(const frame of team.events.follow({kinds:['session.started','session.ended','session.changed','resource.changed']}, {signal:abort.signal,after:cursor,caller:'dispatch session-events'})){
32
- if(frame.type==='snapshot_begin'){snapshot={};connected=false;persist();}
33
- else if(frame.type==='snapshot_item')apply(snapshot??resources,frame);
34
- else if(frame.type==='snapshot_end'){resources=snapshot??resources;snapshot=null;cursor=frame.cursor;connected=true;refused=0;persist();wake('session-events-snapshot');}
35
- else if(frame.type==='event'){const changed=apply(resources,frame);cursor=frame.cursor;connected=true;persist();if(changed)wake('session-events');}
36
- else if(frame.type==='checkpoint'){const recovered=!connected;cursor=frame.cursor;connected=true;refused=0;persist();if(recovered)wake('session-events-reconnected');}
37
- else if(frame.terminal){cursor=null;connected=false;persist();wake('session-events-unavailable');break;}
38
- }
39
- }catch(error){
40
- const asked=retryAfterMs(error);if(asked){pause=Math.min(300000,asked*2**refused);refused++;}
41
- if(!abort.signal.aborted)log(`dispatch session events unavailable: ${error.code??error.message}${asked?` (retrying in ${Math.round(pause/1000)} s)`:''}`);if(error.status===409)cursor=null;
42
- }
43
- connected=false;persist();wake('session-events-unavailable');if(!abort.signal.aborted)await wait(abort.signal,pause);
44
- }
45
- })();
46
- return {close(){abort.abort();connected=false;persist();}};
47
- }
48
-
49
- export function readEventFleet(root){
50
- let cache;try{cache=JSON.parse(readFileSync(pathOf(root),'utf8'));process.kill(cache.pid,0);}catch{return {ok:false,alive:new Set(),online:new Set(),sessions:[],native:new Map(),error:'published session events unavailable',presenceKnown:false};}
51
- const machines=new Map(),online=new Set();
52
- for(const [key,value] of Object.entries(cache.resources??{}))if(key.startsWith('machine:')){machines.set(value.data.id,normalize(value.data.name));if(cache.connected&&value.data.linked)online.add(normalize(value.data.name));}
53
- const sessions=[],native=new Map(),alive=new Set();
54
- for(const [key,value] of Object.entries(cache.resources??{}))if(key.startsWith('session:')){
55
- for(const binding of value.data.bindings??[]){
56
- const machine=machines.get(binding.machine_id);if(!machine||!binding.harness||!binding.native_session_id)continue;
57
- const address=`sc:${machine}:${binding.harness}:${binding.native_session_id}`;
58
- const known=cache.connected&&online.has(machine),state=known?(binding.state==='running'?true:binding.state==='stopped'?false:null):null;
59
- const previous=native.get(address),states=[...(previous?.states??[]),state];
60
- const combined=states.includes(true)?true:states.includes(null)?null:states.every(value=>value===false)?false:null;
61
- native.set(address,{alive:combined,states});if(combined===true)alive.add(address);
62
- sessions.push({address,machine,harness:binding.harness,session_id:binding.native_session_id,pane:binding.pane??null,name:binding.name??null,state:binding.state,turn:binding.turn??null,native_state:binding.evidence?.native_state??null,identity:value.data.agent_identity??null});
63
- }
64
- }
65
- return {ok:!!cache.connected,presenceKnown:!!cache.connected,online,alive,native,sessions,error:cache.connected?null:'published session events disconnected'};
66
- }