@volter/supercode-orchestrator 0.5.36 → 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.
package/agents.mjs CHANGED
@@ -8,6 +8,8 @@ import { createHash, randomBytes } from 'node:crypto';
8
8
  import { runtimeDoor } from './board/runtime-door.mjs';
9
9
  import { resolveAgentProfile } from './agent-profiles.mjs';
10
10
  import { compareAgentProfile } from '@volter/teams/agent-profile';
11
+ import { WorkspaceContexts } from '@volter/teams/context';
12
+ import { supercodeHome } from '@volter/teams/home';
11
13
  const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
12
14
  // The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
13
15
  const HOST_CALLER=(process.env.SUPERCODE_CALLER_NAME??`orchestrator ${process.argv.slice(2).filter(arg=>!arg.startsWith('-')).slice(0,2).filter(arg=>/^[a-z][a-z0-9-]{0,30}$/.test(arg)).join(' ')}`).slice(0,80);
@@ -22,11 +24,9 @@ export function teamsUnavailable(error){
22
24
  if(error?.status!=null)return error.status>=500||error.status===401;
23
25
  return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
24
26
  }
25
- /** The Org agent identity belongs to: the selected Teams context, or this home's local store when there is none. */
26
- export function agentScope(context){
27
- try{return agentDoor('scope',[],{context});}
28
- catch(error){if(teamsUnavailable(error))return {server:'local',org:'local',principal:null,offline:true,reason:error.message};throw error;}
29
- }
27
+ /** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
28
+ * or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
29
+ export function agentScope(context){return agentDoor('scope',[],{context});}
30
30
  function store(root){
31
31
  mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
32
32
  db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
@@ -260,27 +260,42 @@ export function ensureForkAgent(options) {
260
260
  export function agentRunForKey(root,org,agent,key) {
261
261
  const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND agent_id=? AND run_id=? ORDER BY rowid DESC LIMIT 1').get(org,agent,key)??null;}finally{db.close();}
262
262
  }
263
+ const declarationHead=(root,org,key)=>{const db=store(root);try{return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,key)?.agent_id??null;}finally{db.close();}};
264
+ const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
263
265
  function recordDeclaration(root,org,id,declaration){
264
266
  const db=store(root);try{db.prepare('UPDATE agent_intents SET declaration_json=? WHERE org=? AND id=? AND declared=0').run(JSON.stringify(declaration),org,id);}finally{db.close();}
265
267
  }
266
- /** Situation 16: no reachable Teams. The daemon-minted id and retained intent stand in for the declaration,
267
- * which synchronizeLocalAgents later makes under the same id. Nothing is granted from this local record. */
268
+ /** The identity a run starts as without asking Teams: the daemon-minted id and retained intent. An identity not yet
269
+ * declared is declared later under the same id (synchronizeLocalAgents). Nothing is granted from this local record;
270
+ * `identity.synced` says whether Teams has the declaration, and the record's own `synced: false` that Teams was not read. */
268
271
  function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
269
272
  const {root,org,server,profiles,profile,machine=null}=options;
270
273
  const rootProfile=profiles.default?'default':profile;
271
274
  const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
272
275
  recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
273
- const intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
276
+ let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
277
+ // An agent Teams ended is never started again: its end, once synchronized, is on its runs here (synchronizeAgentEnds),
278
+ // and the key's next generation is minted in its place, with fresh memory.
279
+ if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
274
280
  if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
275
281
  const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
276
282
  const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
277
283
  lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
278
- synced:false,main_session:null,revision:0};
284
+ synced:!!intent.declared,main_session:null,revision:0};
279
285
  const assigned=assignAgent(root,org,intent.id,assignment);
280
286
  return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
281
287
  }
282
288
  export function localCardAgent(options){
283
- const {card,role,profile}=options;
289
+ const {root,org,server,profiles,card,role,profile}=options;
290
+ // A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
291
+ // active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
292
+ const held=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
293
+ if(held&&held.agent_id!==declarationHead(root,org,`${server}/${org}/card/${card}/${role}`)&&!endedHere(root,org,held.agent_id)){
294
+ const record=localAgentRecord(root,org,held.agent_id);
295
+ const identity=record?{...record.identity,synced:true}:{id:held.agent_id,kind:'agent',org,name:held.agent_id,path:held.agent_id,parent:null,
296
+ lineage:{starter:null},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',synced:true,main_session:null,revision:0};
297
+ return {identity,intended:record?.intended??resolveAgentProfile(profiles,profile),assignment:held.id,assignments:[{kind:'card',target:card,role}],org,synced:false};
298
+ }
284
299
  return localAgent(options,{key:`card/${card}/${role}`,name:`${card}-${role}`,assignment:{kind:'card',target:card,role},
285
300
  declaration:{kind:'card',card,role,profile,sponsor:options.sponsor??null,starterSession:options.starterSession??null},
286
301
  starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
@@ -459,6 +474,13 @@ export function agentPreamble(agent,{main=true,previous=[]}={}){
459
474
  const {identity:a,intended,assignments=[]}=agent;
460
475
  return `You are agent ${a.path} (${a.id}), ${main?'its main session':'a concurrent session'}.${a.synced===false?' This identity was minted on this machine and is not yet declared on a Teams server; it keeps this id when it is.':''}\nSponsor: ${a.sponsor??'none yet'}; parent: ${a.parent??'none'}; lineage: ${JSON.stringify(a.lineage)}.\nAssignments: ${assignments.map(value=>`${value.kind} ${value.target}, role ${value.role}`).join("; ")}; profile: ${a.profile}; permission layers: ${JSON.stringify(intended.resolved.permissions.layers)}.\nAsk again with supercode agent whoami. List your sessions with supercode agent sessions; read a transcript only when needed.${previous.length?'\nEarlier sessions:\n'+previous.map(s=>`${s.session} · ${s.machine??'unknown machine'} · ended ${s.ended_at??'live'} · ${s.end_reason??''}`).join('\n'):''}`;
461
476
  }
477
+ /** The agent's reader context this home already holds (a session of it was bound here before), read from the contexts
478
+ * file without a request, or null. A launch on this machine hands it to the session, so its Teams verbs run delegated
479
+ * from the first turn; a context that has since expired fails there, as an absent one does. */
480
+ export function localReaderContext(org,id){
481
+ const contexts=new WorkspaceContexts(supercodeHome()).read().contexts??{};
482
+ return Object.entries(contexts).find(([,value])=>value?.principal_id===id&&value?.team_id===org)?.[0]??null;
483
+ }
462
484
  export function agentProfileMismatch(intent,resolved){return compareAgentProfile(intent?.resolved??intent,resolved);}
463
485
 
464
486
  export function agentEnvironment(agent,{root,context,session=null,main=true}={}) {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // The orchestrator's synchronous host reaches Teams through the installed neutral SDK.
3
- import { fromContext } from '@volter/teams/context';
3
+ import { fromContext, WorkspaceContexts } from '@volter/teams/context';
4
4
  import { supercodeHome } from '@volter/teams/home';
5
5
  import { openMachine, normalMachineName } from '@volter/teams/machine-door';
6
6
  import { hostname } from 'node:os';
@@ -11,6 +11,9 @@ const reach=machine=>machine&&normalMachineName(machine)!==normalMachineName(hos
11
11
  let input='';for await(const chunk of process.stdin)input+=chunk;
12
12
  try{
13
13
  const {op,args=[],context}=JSON.parse(input);
14
+ // No context selected: identity is this home's own (Org `local`). A context that is named but unreadable is an error.
15
+ const named=context??process.env.SUPERCODE_CONTEXT??new WorkspaceContexts(supercodeHome()).read().current;
16
+ if(op==='scope'&&(!named||named==='local')){process.stdout.write(JSON.stringify({server:'local',org:'local',principal:null,offline:true,reason:'no Teams context is selected'})+'\n');process.exit(0);}
14
17
  const {client,context:scope}=fromContext({supercodeHome:supercodeHome(),name:context??process.env.SUPERCODE_CONTEXT});
15
18
  const team=client.team(scope.team_id),options={signal:AbortSignal.timeout(20000)};
16
19
  let result;
@@ -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 { agentDoor, agentScope, teamsUnavailable, ensureCardAgent, localCardAgent, 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);
@@ -684,28 +703,20 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
684
703
  // the model it runs is recorded with the session, for caps that count by model
685
704
  const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
686
705
  try {
687
- const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context),machine=normalMachine(task.machine)===localMachine()?null:task.machine;
688
- const cardOptions={root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
689
- profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null,machine,context};
690
- // Situation 16: with no reachable Teams the card's agent is its locally minted identity, declared later under the same id.
691
- let agent=null,previous=null,reader=null;
692
- if(!team.offline){
693
- try{
694
- agent=ensureCardAgent(cardOptions);
695
- previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
696
- reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
697
- }catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
698
- }
699
- agent??=localCardAgent(cardOptions);
706
+ // Teams is never on a launch's path: the card's agent is its identity minted on this host under its retained intent,
707
+ // and its sessions are joined to it off this path, after the launch. No Teams answer starts, stops or holds a card.
708
+ const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context);
709
+ const agent=localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
710
+ profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
700
711
  // G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
701
712
  const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
702
713
  const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
703
714
  if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
704
- previous??=localAgentSessions(board.root,team.org,agent.identity.id);
715
+ const previous=localAgentSessions(board.root,team.org,agent.identity.id);
705
716
  prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
706
717
  Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
707
718
  agent_profile_intent:structuredClone(agent.intended.resolved),
708
- agent_env:agentEnvironment(agent,{root:board.root,context:reader?.context})});
719
+ agent_env:agentEnvironment(agent,{root:board.root,context:normalMachine(task.machine??localMachine())===localMachine()?localReaderContext(team.org,agent.identity.id)??undefined:undefined})});
709
720
  // The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
710
721
  // another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
711
722
  const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
@@ -718,14 +729,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
718
729
  setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
719
730
  event(db,id,'restart_authorization_consumed',{},runId);
720
731
  });
721
- if(result?.session_id){
722
- const ref=`${result.harness}:${result.session_id}`;
723
- recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
724
- if(agent.synced!==false){
725
- try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
726
- catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
727
- }
728
- }
732
+ if(result?.session_id)recordAgentRun(board.root,team.org,agent.identity.id,{session:`${result.harness}:${result.session_id}`,machine:result.machine,run:runId});
729
733
  return result;
730
734
  } catch(error) {
731
735
  // An issued asynchronous launch may bind later. Its original permission
@@ -751,7 +755,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
751
755
  export async function startCard(board, id, { roles = ['manager'], lanes, settings } = {}) {
752
756
  // the board's own published fleet projection, as a tick reads it: a manager's or creator's start that continues the
753
757
  // slot's session checks that session through it
754
- eventFleetRoot = board.root;
758
+ fleetRoot = board.root;
755
759
  const entry = await loadWorkflow(board.root);
756
760
  const fleet = observeFleet();
757
761
  const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
@@ -948,8 +952,11 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
948
952
  const runRow = currentRun(db, id);
949
953
  const session = runRow?.metadata?.supercode;
950
954
  if (!session?.session_id || !['pane', 'native'].includes(session.surface) || session.offline_since != null) return;
951
- if (session.surface === 'native' && fleet.native?.get(addressOf(session))?.alive !== true) return;
952
- 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);
953
960
  const observedAt = now();
954
961
  const lastAtMs = Number(live?.last_message_at_ms) || updatedAt(session);
955
962
  const lastAt = Math.floor(Number(lastAtMs) / 1000);
@@ -1004,7 +1011,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
1004
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 });
1005
1012
  event(db, id, 'session_idle_observed', observation, runRow.id);
1006
1013
  if (after?.id === runRow.id) setRunSession(db, runRow.id, { goal_nudged_at: observedAt });
1007
- report.notified.push([id, 'session_idle', live.address]);
1014
+ report.notified.push([id, 'session_idle', addressOf(session)]);
1008
1015
  } catch (error) {
1009
1016
  retain('dispatch_failed', { error: String(error.message).slice(-200) });
1010
1017
  report.failed.push([id, `session_idle: ${String(error.message).slice(-200)}`]);
@@ -1136,7 +1143,7 @@ function launcherCheck(db, task, lane, fleet) {
1136
1143
  if (!task.machine) return 'the card names no machine; a pane session runs on the machine its card names';
1137
1144
  const machine = normalMachine(task.machine);
1138
1145
  if (!fleet.ok) return `supercode could not read the fleet: ${fleet.error}`;
1139
- 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`;
1140
1147
  if (!fleet.online.has(machine)) return `machine ${machine} is offline; the card starts when it is back`;
1141
1148
  // a card on this machine gets its workspace made here (scratch, dir or worktree, as Hermes makes it); another
1142
1149
  // machine's card names its directory there
@@ -1281,7 +1288,7 @@ function bindLaunch(board, row, session, pane, sid) {
1281
1288
 
1282
1289
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
1283
1290
  export function reconcileCallerLaunch(board, sid) {
1284
- eventFleetRoot = board.root;
1291
+ fleetRoot = board.root;
1285
1292
  if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
1286
1293
  WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
1287
1294
  const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
@@ -1314,15 +1321,6 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1314
1321
  for(const row of agentRuns){
1315
1322
  const session=json(row.metadata,{})?.supercode;
1316
1323
  if(!session?.session_id||!session.agent_org)continue;
1317
- const observation=fleet.sessions?.find(item=>item.address===addressOf(session))?.identity;
1318
- if(observation?.agent?.id===session.agent_id&&session.agent_profile_revision!==observation.revision){
1319
- const mismatch=agentProfileMismatch(session.agent_profile_intent,observation.resolved);
1320
- tx(board,db=>{
1321
- // Rule 4: a run whose recorded profile differs from its agent's intent is shown and not counted.
1322
- setRunSession(db,row.id,{agent_profile_recorded:observation.resolved,agent_profile_revision:observation.revision,agent_profile_mismatch:mismatch,counted:!mismatch.differences.length});
1323
- 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);
1324
- });
1325
- }
1326
1324
  const ref=`${session.harness}:${session.session_id}`;
1327
1325
  // An ended run whose session the agent store already holds, published, for another agent lost that claim when it
1328
1326
  // ran: no round can change an ended run or a published owner, so the disagreement is history. It is said once on
@@ -1342,7 +1340,8 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1342
1340
  orgs.set(session.agent_org,session.agent_context);
1343
1341
  }
1344
1342
  // Identities minted while Teams was absent are declared under their ids before their sessions are joined.
1345
- 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}`]);}
1346
1345
  if(!scope.offline){
1347
1346
  orgs.set(scope.org,orgs.get(scope.org)??context);
1348
1347
  for(const pending of synchronizeLocalAgents({root:board.root,profiles:entry.profiles,context}))report.queued.push([pending.agent,`agent declaration pending: ${pending.error}`]);
@@ -1451,7 +1450,8 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1451
1450
  if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
1452
1451
  const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
1453
1452
  const how = HARNESSES[harness];
1454
- 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',
1455
1455
  ...Object.entries(meta.agent_env??{}).flatMap(([key,value])=>['--env',`${key}=${value}`])];
1456
1456
  // the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
1457
1457
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
@@ -1512,10 +1512,14 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1512
1512
  };
1513
1513
  let session = null;
1514
1514
  let replaced = null;
1515
- 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)) {
1516
1519
  const reading = nativeReading(nativeSessions(prev.machine, prev.harness), prev);
1517
1520
  if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1518
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`);
1519
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`);
1520
1524
  }
1521
1525
  if (prev?.forgotten) {
@@ -1765,25 +1769,13 @@ function adoptionOpenTo(db, id, request, open) {
1765
1769
  return Boolean(claim) && claim.request === request && claim.lock === claimer();
1766
1770
  }
1767
1771
 
1768
- /**
1769
- * A session on this machine that Teams never published (one outside every synced root: a profile's own directory) is
1770
- * found through this machine's own doors: its panes, and the native session each pane's process runs. Only a full
1771
- * address is looked up here; liveness is still verified below as for any other session.
1772
- */
1773
- 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) {
1774
1774
  const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(who));
1775
- if (!m || normalMachine(m[1]) !== localMachine()) return null;
1776
- const [, , harness, sessionId] = m;
1777
- const listed = runtimeDoor(null, 'harness.v1.panes.list', {}, { timeout: 60_000 });
1778
- const panes = (listed.code === 0 ? json(listed.stdout.trim(), []) : []) ?? [];
1779
- const live = panes.filter((p) => !p.dead && p.pid);
1780
- if (!live.length) return null;
1781
- const r = runtimeDoor(null, 'harness.v1.sessions.activity_under', { pids: live.map((p) => p.pid) }, { timeout: 30_000 });
1782
- const row = (r.code === 0 ? json(r.stdout.trim(), {})?.activities ?? [] : []).find((a) => a.activity?.session_id === sessionId && (a.activity?.harness ?? harness) === harness);
1783
- // Not found in a pane of this machine: not listed, so a session opened moments ago is looked for again next tick.
1784
- const pane = row ? live.find((p) => p.pid === row.pid)?.pane ?? null : null;
1785
- if (!pane) return null;
1786
- 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; }
1787
1779
  }
1788
1780
 
1789
1781
  /** The verb an adoption with no current run fires: the workflow's own `adopt` where it declares one, else its dispatch start. */
@@ -1805,7 +1797,7 @@ function adoptedRole(db, board, entry, task, run) {
1805
1797
  function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1806
1798
  fleet ??= observeFleet();
1807
1799
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1808
- 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);
1809
1801
  if (!found) throw Object.assign(new Error(`the fleet lists no live session ${who}`), { notListed: true });
1810
1802
  const [, host, harness, sessionId] = found.address.split(':');
1811
1803
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
@@ -1831,7 +1823,7 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1831
1823
  const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
1832
1824
  const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
1833
1825
  const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
1834
- // The request is claimed before this adoption's first write outside the board (the agent's Teams record, below), in
1826
+ // The request is claimed before this adoption's first write outside the board (the agent store, below), in
1835
1827
  // a transaction that finds it still open and the card's run the one read: a request answered or claimed meanwhile, or
1836
1828
  // a run that changed, ends it here with nothing written anywhere, and no other round acts on a request claimed here.
1837
1829
  const stale = (db) => {
@@ -1845,11 +1837,8 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1845
1837
  event(db, id, 'adoption_claimed', { request, session: who, ...newClaim(ttl) });
1846
1838
  });
1847
1839
  progress.claimed = request !== null;
1848
- let agent = null;
1849
- if (lane && !team.offline) {
1850
- try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
1851
- catch (error) { if (!teamsUnavailable(error)) throw error; }
1852
- }
1840
+ // The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
1841
+ const agent = lane ? localCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null }) : null;
1853
1842
  // A session another agent already owns is refused before anything is recorded: the run would name one agent and the
1854
1843
  // agent store another, and every round after would trip on it.
1855
1844
  const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
@@ -2123,7 +2112,7 @@ function endWorkerSubscriptions(board, fleet, report, natively = new Map()) {
2123
2112
  const ended = new Map();
2124
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()) {
2125
2114
  const address = row.chat_id, record = latest.get(address);
2126
- 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;
2127
2116
  const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at } : natively.get(address);
2128
2117
  if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
2129
2118
  if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
@@ -2150,7 +2139,7 @@ async function notify(board, fleet, report, entry) {
2150
2139
  // a session the fleet does not list this tick is told next tick: one reading is not a loss (a loaded box drops
2151
2140
  // sessions from a read, and a dropped subscription never came back), and a session that is replaced on its card
2152
2141
  // has its subscription moved when the new one starts
2153
- if (role !== 'creator' && fleet.ok && !fleet.alive.has(sub.chat_id)) return;
2142
+ if (role !== 'creator' && fleet.ok && !subscriberAlive(fleet, sub.chat_id)) return;
2154
2143
  const kinds = heardBy(entry, role);
2155
2144
  if (!kinds.length) return;
2156
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`)
@@ -2477,7 +2466,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
2477
2466
 
2478
2467
  /** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
2479
2468
  export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
2480
- eventFleetRoot = root;
2469
+ fleetRoot = root;
2481
2470
  const loaded = await loadWorkflow(root, { fresh: true });
2482
2471
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
2483
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' };
@@ -2587,10 +2576,10 @@ function capMax(cap, group, entry, lanes) {
2587
2576
  * effects in `runEffects` (send_to_actor, session_notice, merge_reviewed, declare_card_agent), board mail,
2588
2577
  * subscription notices (a doubled notice_delivered has been seen live), deferred-launch retries (two panes),
2589
2578
  * launch-receipt work (cancelUnissuedLaunch, bindLaunch), the Teams agent-run sync in reconcileLaunches, and
2590
- * 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.
2591
2580
  */
2592
2581
  export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
2593
- eventFleetRoot = root;
2582
+ fleetRoot = root;
2594
2583
  // a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
2595
2584
  const deployment = deploymentReceipt ? workflowRevision(root) : null;
2596
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/index.d.ts CHANGED
@@ -128,10 +128,10 @@ export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: str
128
128
  export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
129
129
  export function agentPreamble(agent: ReturnType<typeof ensureCardAgent>, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
130
130
  export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
131
- /** The Org for agent identity; `offline` when no Teams context is selected or reachable (situation 16). */
131
+ /** The Org for agent identity, read from this home's selected context without a request; `offline` when none is selected. Throws when a named context cannot be read. */
132
132
  export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
133
133
  export function teamsUnavailable(error: unknown): boolean;
134
- /** Locally minted identities (`identity.synced === false`), declared later under the same id. */
134
+ /** The identity a run starts as without asking Teams; `identity.synced` is false until Teams has its declaration, made later under the same id. */
135
135
  export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
136
136
  export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
137
137
  export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.36",
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
- }