@volter/supercode-orchestrator 0.5.73 → 0.5.74

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.
@@ -28,9 +28,9 @@ import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, review
28
28
  import { runtimeDoor } from './runtime-door.mjs';
29
29
  import { agentScope, localCardAgent, localReaderContext, localAgentSessions, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, activeAgentAssignments,finishAgentAssignment,handOffAgents, agentRunForSession } from '../agents.mjs';
30
30
  import { materializeAgentHome } from '../agent-home.mjs';
31
- import { memorySnapshotSync } from '../hermes-cli.mjs';
31
+ import { memorySnapshot } from '../hermes-cli.mjs';
32
32
  import { createRequire } from 'node:module';
33
- import { spawn as spawnChild, spawnSync } from 'node:child_process';
33
+ import { spawn as spawnChild, execFile } from 'node:child_process';
34
34
  import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
35
35
  import { homedir, hostname, tmpdir, totalmem } from 'node:os';
36
36
  import { basename, dirname, join } from 'node:path';
@@ -81,14 +81,27 @@ export function commandEnv(as = null) {
81
81
  return { ...process.env, ...(as ? { SUPERCODE_CALLER: as } : {}) };
82
82
  }
83
83
 
84
+ /**
85
+ * A `supercode` command the board runs as itself (a card's pane started or closed: `open … --pane`, `close`), started
86
+ * and awaited without holding this process's event loop: the owner answers verbs while it runs. `as`: the board this
87
+ * command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb.
88
+ * (Gap A31: these two are still helper processes; the daemon's launch and close doors replace them.)
89
+ */
84
90
  function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
85
- // `as`: the board this
86
- // command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
87
91
  const env = commandEnv(as);
88
92
  const started = performance.now();
89
- const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
90
- noteStep(`supercode ${args[0]}`, performance.now() - started);
91
- return { code: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? r.error.message : '') };
93
+ return new Promise((resolve) => {
94
+ let stdout = '', stderr = '', done = false;
95
+ const child = spawnChild(supercodeBin(), args, { cwd: tmpdir(), env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
96
+ const finish = (code, extra = '') => { if (done) return; done = true; clearTimeout(timer); noteStep(`supercode ${args[0]}`, performance.now() - started); resolve({ code, stdout, stderr: stderr + extra }); };
97
+ const timer = setTimeout(() => { child.kill('SIGKILL'); finish(null, `supercode ${args[0]} did not answer within ${timeout} ms`); }, timeout);
98
+ child.stdout.setEncoding('utf8').on('data', (chunk) => { stdout += chunk; });
99
+ child.stderr.setEncoding('utf8').on('data', (chunk) => { stderr += chunk; });
100
+ child.on('error', (error) => finish(null, error.message));
101
+ child.on('close', (code) => finish(code));
102
+ child.stdin.on('error', () => {});
103
+ child.stdin.end(input ?? undefined);
104
+ });
92
105
  }
93
106
 
94
107
  const lastJson = (text) => {
@@ -122,7 +135,7 @@ export function callerSession() {
122
135
 
123
136
  /** The fleet as the home's machines answer for themselves (machine-fleet.mjs): this machine, and every machine an open
124
137
  * card of the home names or one of its notice subscribers is on. */
125
- export function observeFleet(root = fleetRoot) {
138
+ export async function observeFleet(root = fleetRoot) {
126
139
  const machines = new Set();
127
140
  for (const slug of root ? listBoards(root) : []) {
128
141
  try {
@@ -134,7 +147,7 @@ export function observeFleet(root = fleetRoot) {
134
147
  } finally { b.close(); }
135
148
  } catch { /* its own tick reports it */ }
136
149
  }
137
- const fleet = readMachineFleet(machines, localMachine());
150
+ const fleet = await readMachineFleet(machines, localMachine());
138
151
  presenceReading = { known: fleet.presenceKnown === true, online: new Set([localMachine(), ...fleet.online]), unknown: fleet.unknown ?? new Set() };
139
152
  notePresence(presenceReading);
140
153
  return fleet;
@@ -155,7 +168,6 @@ const HARNESSES = {
155
168
  const doorTransient = (r) => r.code === null || ['unavailable', 'machine_offline', 'rate_limited', 504, '504', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN'].includes(json(String(r.stderr ?? '').trim().split('\n').pop(), null)?.code);
156
169
  const adoptedOnly = (s) => s?.mode === 'adopt' && !s.launch_key && s.surface !== 'headless';
157
170
  const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
158
- const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
159
171
 
160
172
  // 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
161
173
  // 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
@@ -163,8 +175,16 @@ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0,
163
175
  const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : unpublishedAlive(session);
164
176
  /** A subscriber's address as the session it names, for sessionAlive (its machine, harness and native id). */
165
177
  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; };
166
- const subscriberAlive = (fleet, address) => fleet.alive.has(address) || (sessionAt(address) ? sessionAlive(fleet, sessionAt(address)) === true : false);
178
+ /** A subscriber's liveness, read first (readBefore): true, false, or null when it could not be read (unknown, never dead). */
179
+ async function subscriberAlive(fleet, address) {
180
+ if (fleet.alive.has(address)) return true;
181
+ const session = sessionAt(address);
182
+ if (!session) return false;
183
+ await readBefore(fleet, session);
184
+ return sessionAlive(fleet, session) ?? null;
185
+ }
167
186
  const paneLists = new Map();
187
+ /** A session the fleet reading did not publish: alive by what was read of it before (readBefore), else unknown (null). */
168
188
  function unpublishedAlive(session) {
169
189
  if (!session) return null;
170
190
  if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
@@ -173,50 +193,59 @@ function unpublishedAlive(session) {
173
193
  if (!session.session_id) return null;
174
194
  try { return nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id), session).alive; } catch { return null; }
175
195
  }
176
- let entry = paneLists.get(session.machine);
177
- if (!entry || Date.now() - entry.at > READING_MS) {
178
- if (readingsOnly) return null;
179
- const r = doorOn(session.machine, 'harness.v1.panes.list', {}, { timeout: 60_000 });
180
- const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
181
- entry = { at: Date.now(), panes: Array.isArray(panes) ? panes : null };
182
- paneLists.set(session.machine, entry);
183
- }
196
+ const entry = paneLists.get(session.machine);
197
+ if (!entry || Date.now() - entry.at > READING_MS) return null;
184
198
  return entry.panes ? entry.panes.some((p) => p.pane === session.pane && !p.dead) : null;
185
199
  }
200
+ /** A machine's pane list, read through its door (one reading serves READING_MS). */
201
+ async function readPaneList(machine) {
202
+ const entry = paneLists.get(machine);
203
+ if (entry && Date.now() - entry.at <= READING_MS) return entry;
204
+ const r = await doorOn(machine, 'harness.v1.panes.list', {}, { timeout: 60_000 });
205
+ const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
206
+ const read = { at: Date.now(), panes: Array.isArray(panes) ? panes : null };
207
+ paneLists.set(machine, read);
208
+ return read;
209
+ }
186
210
 
187
211
  /** Native activity comes from the machine's harness discovery, not catalogue age: each session the round reads is
188
212
  * discovered by its id (about 0.2 s), once per round (tick clears them). Listing the machine's every session to find
189
213
  * them (up to 2000, 1-6 s each) cost more than the sessions a board runs. */
190
214
  const discoveries = new Map();
191
215
  // A machine's pane list and harness discovery are read before a board write opens, never inside one: a door that
192
- // takes seconds would hold the board's write lock past a worker's own command's busy wait. A write that needs a
193
- // reading not taken yet reads it as unknown. One reading serves READING_MS.
216
+ // takes seconds would hold the board's write lock past a worker's own command's busy wait. Reading is async (readNative,
217
+ // readPaneList, readBefore: the doors in this process, never holding the event loop); every decision reads what was
218
+ // read (nativeSessions, unpublishedAlive, sessionAlive), and a reading not taken yet is unknown. One reading serves
219
+ // READING_MS.
194
220
  const READING_MS = 15_000;
195
- let readingsOnly = 0;
196
- const txRead = (board, fn) => { readingsOnly += 1; try { return tx(board, fn); } finally { readingsOnly -= 1; } };
197
- /** Take the readings a board write about `session` will need, before the write. */
198
- function readBefore(fleet, session) {
199
- if (!session?.session_id || !session.machine || session.surface === 'headless') return;
200
- try { nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id); } catch { /* unread: unknown inside the write */ }
201
- sessionAlive(fleet, session);
202
- }
221
+ const txRead = (board, fn) => tx(board, fn);
222
+ /** Take the readings a decision or a board write about `session` will need, before it. */
223
+ async function readBefore(fleet, session) {
224
+ if (!session?.machine || session.surface === 'headless') return;
225
+ if (session.session_id) try { await readNative(session.machine, session.harness ?? 'claude-code', session.session_id); } catch { /* unread: unknown */ }
226
+ if (session.pane && !fleet?.native?.has(addressOf(session))) await readPaneList(session.machine);
227
+ }
228
+ /** A session's native discovery as this round read it (readNative); throws (unavailable) when it was not read. */
203
229
  function nativeSessions(machine, harness, sessionId) {
230
+ const seen = discoveries.get(nativeKey(machine, harness, sessionId));
231
+ if (seen) return seen.sessions;
232
+ throw unavailable('native liveness is read before it is used (readBefore), and was not');
233
+ }
234
+ /** Read a session's native discovery through its machine's door, once a round. */
235
+ async function readNative(machine, harness, sessionId) {
204
236
  const key = nativeKey(machine, harness, sessionId), seen = discoveries.get(key);
205
237
  if (seen) return seen.sessions;
206
- if (readingsOnly) throw unavailable('native liveness is read before a board write, not inside it');
207
- const sessions = discoverNative(machine, harness, sessionId);
238
+ const sessions = await discoverNative(machine, harness, sessionId);
208
239
  discoveries.set(key, { at: Date.now(), sessions });
209
240
  return sessions;
210
241
  }
211
242
  // A native discovery is bounded as the fleet reading is: one that does not answer in time is unknown, read next round.
212
243
  const NATIVE_DISCOVERY_MS = 15_000;
213
244
  const nativeKey = (machine, harness, sessionId) => `${normalMachine(machine)}\u0000${harness}\u0000${sessionId}`;
214
- function discoverNative(machine, harness, sessionId) {
245
+ async function discoverNative(machine, harness, sessionId) {
215
246
  if (offlineMachine(machine)) throw unavailable(`native liveness unavailable: machine ${normalMachine(machine)} is offline`);
216
- // This machine's discovery is its harness door, as every other harness read here is; another machine's is supercode's fleet arm
217
- const result = normalMachine(machine) === localMachine()
218
- ? doorOn(machine, 'harness.v1.sessions.discover', { harnesses: [harness], query: sessionId, limit: 2000 }, { timeout: NATIVE_DISCOVERY_MS })
219
- : sc(['discover', '--on', machine, '--harness', harness, '--query', sessionId, '--limit', '2000'], { timeout: NATIVE_DISCOVERY_MS });
247
+ // the machine's own harness door, this one's or another's, as every other harness read here is
248
+ const result = await doorOn(machine, 'harness.v1.sessions.discover', { harnesses: [harness], query: sessionId, limit: 2000 }, { timeout: NATIVE_DISCOVERY_MS });
220
249
  const page = result.code === 0 ? lastJson(result.stdout) : null;
221
250
  if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
222
251
  return page.sessions;
@@ -299,27 +328,28 @@ const offlineMachine = (machine) => {
299
328
  return m !== localMachine() && presenceReading?.known === true && !presenceReading.online.has(m);
300
329
  };
301
330
  /** A machine's public door (`harness.v1.*`), answered at once when the machine reads as offline. */
302
- const doorOn = (machine, method, params, options) => offlineMachine(machine)
331
+ const doorOn = async (machine, method, params, options) => offlineMachine(machine)
303
332
  ? { code: 1, stdout: '', stderr: JSON.stringify({ code: 'machine_offline', message: `machine ${normalMachine(machine)} did not answer the fleet reading` }) }
304
- : runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
333
+ : await runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
305
334
  const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
306
335
 
307
336
  // ------------------------------------------------------------------ supervise
308
337
 
309
- function supervise(board, fleet, settings, report, entry, effects) {
338
+ async function supervise(board, fleet, settings, report, entry, effects) {
310
339
  const rows = board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all();
311
340
  // Published native observations are the only liveness input. Missing data remains unknown. A launch the
312
341
  // observations show stopped is asked, through its machine's door, how it ended.
313
342
  const exits = new Map();
343
+ // every running session's liveness is read before anything is decided of it (unknown stays unknown)
344
+ for (const { id } of rows) await readBefore(fleet, currentRun(board.db, id)?.metadata?.supercode);
314
345
  for (const { id } of rows) {
315
346
  const session = currentRun(board.db, id)?.metadata?.supercode;
316
347
  if (!session?.launch_key || !fleet.ok || sessionAlive(fleet, session) !== false) continue;
317
348
  // how a keyed launch ended, where its machine keeps that record: read when it can be, never waited on
318
349
  if (!fleet.capabilities?.get(normalMachine(session.machine))?.launches) continue;
319
- try { const receipt = launchReceipt(board, session); if (receipt?.live === false && receipt.exit) exits.set(id, { ...receipt.exit, launch_key: session.launch_key }); }
350
+ try { const receipt = await launchReceipt(board, session); if (receipt?.live === false && receipt.exit) exits.set(id, { ...receipt.exit, launch_key: session.launch_key }); }
320
351
  catch (error) { report.failed.push([id, `exit receipt unavailable; retaining liveness observation: ${error.message}`]); }
321
352
  }
322
- for (const { id } of rows) readBefore(fleet, currentRun(board.db, id)?.metadata?.supercode);
323
353
  for (const { id } of rows) {
324
354
  // A transaction that meets another round's write runs again from its start: what it asks for is staged per attempt
325
355
  // and kept only once it committed, so a second round never carries out a move that was rolled back.
@@ -490,29 +520,29 @@ function limitsOf(db, board, entry, task, session) {
490
520
  return out;
491
521
  }
492
522
 
493
- function stopSession(board, session) {
523
+ async function stopSession(board, session) {
494
524
  if (!session) return;
495
525
  if (boardCopyOf(board)) throw copyRefusal(board, 'closes no session');
496
526
  if (session.surface === 'headless' && pidAlive(session.pid)) {
497
527
  // SIGTERM to the worker's process group, then SIGKILL if it is still there after a grace, as Hermes stops its own
498
528
  const signal = (sig) => { try { process.kill(-session.pid, sig); } catch { try { process.kill(session.pid, sig); } catch { /* gone */ } } };
499
529
  signal('SIGTERM');
500
- for (let waited = 0; waited < 10 && pidAlive(session.pid); waited += 1) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
530
+ for (let waited = 0; waited < 10 && pidAlive(session.pid); waited += 1) await new Promise((resolve) => setTimeout(resolve, 500));
501
531
  if (pidAlive(session.pid)) signal('SIGKILL');
502
532
  return;
503
533
  }
504
534
  if (session.surface !== 'headless' && offlineMachine(session.machine)) throw new Error(`close deferred: machine ${normalMachine(session.machine)} is offline`);
505
535
  if (!session.pane && session.surface === 'native') {
506
- const r = sc(['close', addressOf(session)], { timeout: 120_000, as: boardAddress(board.label) });
536
+ const r = await sc(['close', addressOf(session)], { timeout: 120_000, as: boardAddress(board.label) });
507
537
  if (r.code !== 0) throw new Error(`${doorTransient(r) ? 'close deferred: ' : ''}closing native session ${addressOf(session)}: ${(r.stderr || r.stdout).trim().slice(-500)}`);
508
538
  // the close's receipt is a discovery taken after it, never the round's cached one from before
509
539
  discoveries.delete(nativeKey(session.machine, session.harness, session.session_id));
510
- const reading = nativeReading(nativeSessions(session.machine, session.harness, session.session_id), session);
540
+ const reading = nativeReading(await readNative(session.machine, session.harness, session.session_id), session);
511
541
  if (reading.alive !== false) throw new Error(`native close has no confirmed stopped receipt for ${addressOf(session)}`);
512
542
  return;
513
543
  }
514
544
  if (session.pane) {
515
- const r = runtimeDoor(normalMachine(session.machine??localMachine())===localMachine()?null:session.machine,'harness.v1.panes.close',{pane:session.pane},{env:commandEnv(),timeout:60_000,as:boardAddress(board.label)});
545
+ const r = await runtimeDoor(normalMachine(session.machine??localMachine())===localMachine()?null:session.machine,'harness.v1.panes.close',{pane:session.pane},{env:commandEnv(),timeout:60_000,as:boardAddress(board.label)});
516
546
  // a door that timed out or could not be reached says nothing about the pane: the close waits for the next round
517
547
  const transient = doorTransient(r);
518
548
  if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`${transient ? 'close deferred: ' : ''}closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
@@ -531,7 +561,7 @@ export function whereOf(board, task) {
531
561
  const hostOf = (pressure) => ({ memory_pressure: pressure, max_in_progress: derivedMaxInProgress() });
532
562
 
533
563
  /** What every board of the home at `root` holds now, counted for its caps (capacity.mjs). */
534
- function homeHoldings(root, slugs, lanes, fleet, entry) {
564
+ async function homeHoldings(root, slugs, lanes, fleet, entry) {
535
565
  const live = (s) => fleet.sessions?.find((x) => x.address === addressOf(s)) ?? null;
536
566
  const out = [];
537
567
  for (const slug of slugs) {
@@ -539,6 +569,8 @@ function homeHoldings(root, slugs, lanes, fleet, entry) {
539
569
  const b = openBoard(root, slug);
540
570
  try {
541
571
  b.slug ??= slug; b.label ??= boardLabel(root, slug);
572
+ // each run's session read before it is counted live or not (unknown stays unknown)
573
+ for (const r of b.db.prepare('SELECT r.metadata FROM tasks t JOIN task_runs r ON r.id = t.current_run_id').all()) await readBefore(fleet, json(r.metadata, {})?.supercode);
542
574
  const storedOf = (id) => storedStatus(b.db, b, entry, id).stored;
543
575
  out.push(...holdings(b, { lanes, fleetOk: fleet.ok, alive: (s) => sessionAlive(fleet, s), liveRecord: live, modelOf: transcriptModel, storedOf, machineOf: normalMachine, home: localMachine() }));
544
576
  }
@@ -597,7 +629,7 @@ function transcriptModel(session) {
597
629
  * that matches a card must have room; capacity.mjs). Each is sent the start event (its validators are Hermes's respawn
598
630
  * guard) and what its new status runs is launched. `held` is what the home holds (homeHoldings), shared by its boards.
599
631
  */
600
- function startCards(board, fleet, lanes, settings, report, entry, effects, { only = null, held = [], dry = false } = {}) {
632
+ async function startCards(board, fleet, lanes, settings, report, entry, effects, { only = null, held = [], dry = false } = {}) {
601
633
  const stop = paused(board.root);
602
634
  if (stop) { report.paused = stop; return; }
603
635
  const { workflow, params } = entry;
@@ -671,7 +703,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
671
703
  }
672
704
  let moved, checked;
673
705
  try {
674
- checked = checkWorkspace(board, task);
706
+ checked = await checkWorkspace(board, task);
675
707
  moved = tx(board, (db) => {
676
708
  // The claim: another round (a second serve, a one-shot dispatch) may have started this card, or others that count
677
709
  // against the same caps, since this round read the board. Its starts are counted, and this card starts only if it
@@ -702,32 +734,75 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
702
734
  effects.push(...moved.effects);
703
735
  caps.take(waitingEntries[i]);
704
736
  try {
705
- const session = launchRun(board, entry, task.id, plan.lane, fleet, settings);
737
+ const session = await launchRun(board, entry, task.id, plan.lane, fleet, settings);
706
738
  if (session) {
707
739
  if (session.surface !== 'pane' || normalMachine(task.machine ?? '') === localMachine()) tickState.local_started += 1;
708
740
  report.started.push([task.id, session.address ?? `pid ${session.pid}`, session.mode]);
709
741
  }
710
742
  } catch (error) {
711
- if (error.launchPending) { report.queued.push([task.id, error.message]); continue; }
743
+ // a launch whose run ended meanwhile is no failure of the card's current run (its successor): said, nothing fired
744
+ if (error.launchPending || error.launchSuperseded) { report.queued.push([task.id, error.message]); continue; }
712
745
  const payload = { error: String(error.message).slice(-500), origin: originOf(error), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
713
- try { const r = tx(board, db => monitorsRuns(entry) ? (surfaceIncident(db,board,entry,task.id,'spawn_failed',payload), {effects:[]}) : fire(db, board, entry, task.id, 'spawn_failed', { roles: ['platform'], payload })); effects.push(...r.effects); } catch { /* no transition for it */ }
746
+ let r;
747
+ try { r = launchFailed(board, entry, task.id, error, payload); } catch { r = { effects: [] }; /* no transition for it */ }
748
+ if (!r) { report.queued.push([task.id, `run ${error.launchRun} was replaced before its launch failed: ${error.message}`]); continue; }
749
+ effects.push(...r.effects);
714
750
  report.failed.push([task.id, String(error.message).slice(-300)]);
715
751
  }
716
752
  }
717
753
  }
718
754
 
719
755
  /** Launch what the card's status runs: its role's lane, in the run's session slot, told the run's prompt and doors. */
720
- export function launchRun(board, entry, id, lane, fleet, settings) {
756
+ /** Whether `runId` is still card `taskId`'s current, unended run: a launch's permission, read in the write it is used in. */
757
+ function runCurrent(db, taskId, runId) {
758
+ return db.prepare('SELECT current_run_id FROM tasks WHERE id = ?').get(taskId)?.current_run_id === runId
759
+ && db.prepare('SELECT ended_at FROM task_runs WHERE id = ?').get(runId)?.ended_at == null;
760
+ }
761
+ const superseded = (taskId, runId) => Object.assign(new Error(`run ${runId} of ${taskId} ended or was replaced while its launch was prepared; nothing was started`), { launchSuperseded: true });
762
+ /** A launch's preparation awaits (minting, workspace, doors) while the owner answers verbs: after each await, and right
763
+ * before it is issued, the run must still be the card's current, unended one (a cancel or restart meanwhile ends it). */
764
+ function assertRunCurrent(board, taskId, runId) {
765
+ if (!view(board, (db) => runCurrent(db, taskId, runId))) throw superseded(taskId, runId);
766
+ }
767
+ /** A write a launch makes for its run: made only while that run is still the card's current one, checked in the same
768
+ * write, so a superseded launch's late result never lands on its successor's card. */
769
+ function runTx(board, taskId, runId, fn) {
770
+ return tx(board, (db) => { if (!runCurrent(db, taskId, runId)) throw superseded(taskId, runId); return fn(db); });
771
+ }
772
+ /** A launch's failure, recorded against the run it attempted and only while that run is still the card's current one
773
+ * (checked in the write that records it): a superseded launch's failure is no failure of its successor. Null when it
774
+ * was superseded. */
775
+ function launchFailed(board, entry, id, error, payload) {
776
+ return tx(board, (db) => {
777
+ if (!runCurrent(db, id, error.launchRun)) return null;
778
+ if (monitorsRuns(entry)) { surfaceIncident(db, board, entry, id, 'spawn_failed', payload, error.launchRun); return { effects: [] }; }
779
+ return fire(db, board, entry, id, 'spawn_failed', { roles: ['platform'], payload });
780
+ });
781
+ }
782
+
783
+ /** The card's current run's launch. Every error it ends with names that run (`launchRun`); one read when the run is no
784
+ * longer the card's current one is its supersession, whatever failed, so no caller reports it against the successor. */
785
+ export async function launchRun(board, entry, id, lane, fleet, settings) {
786
+ const runId = getTaskNow(board, id).current_run_id;
787
+ try { return await launchAttempt(board, entry, id, lane, fleet, settings, runId); }
788
+ catch (error) {
789
+ error.launchRun = runId;
790
+ if (!error.launchSuperseded && !view(board, (db) => runCurrent(db, id, runId))) error.launchSuperseded = true;
791
+ throw error;
792
+ }
793
+ }
794
+
795
+ async function launchAttempt(board, entry, id, lane, fleet, settings, runId) {
721
796
  let task = getTaskNow(board, id);
722
- const checked = checkWorkspace(board, task);
723
- if (checked) tx(board, (db) => {
797
+ if (task.current_run_id !== runId) throw superseded(id, runId);
798
+ const checked = await checkWorkspace(board, task);
799
+ if (checked) runTx(board, id, runId, (db) => {
724
800
  assertWorkspaceOwner(db, task, checked);
725
801
  db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
726
802
  });
727
803
  const status = view(board, (db) => statusOf(db, task));
728
804
  const spec = runSpec(entry, status, task);
729
805
  if (!spec) return null;
730
- const runId = task.current_run_id;
731
806
  const surface = lane.surface === 'pane' ? 'pane' : 'headless';
732
807
  const scope = view(board, (db) => scopeOf(db, board, task, { params: entry.params }));
733
808
  const where = whereOf(board, task);
@@ -748,8 +823,9 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
748
823
  // Teams is never on a launch's path: the card's agent is its identity minted on this host under its retained intent,
749
824
  // and its sessions are joined to it off this path, after the launch. No Teams answer starts, stops or holds a card.
750
825
  const context=current().facts.SUPERCODE_CONTEXT,team=agentScope(context);
751
- const agent=localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
826
+ const agent=await localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
752
827
  profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
828
+ assertRunCurrent(board,id,runId);
753
829
  // G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
754
830
  const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
755
831
  const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
@@ -762,12 +838,14 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
762
838
  // The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
763
839
  // another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
764
840
  const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
841
+ assertRunCurrent(board,id,runId);
765
842
  if(surface==='headless')meta.agent_env.HERMES_HOME=agentHome;
766
- else{const memory=memorySnapshotSync(agentHome);if(memory)prompt=`${prompt}\n\n${memory}`;}
843
+ else{const memory=await memorySnapshot(agentHome).catch(()=>'');assertRunCurrent(board,id,runId);if(memory)prompt=`${prompt}\n\n${memory}`;}
767
844
  const result = surface === 'pane'
768
- ? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
769
- : launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
845
+ ? await launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
846
+ : await launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
770
847
  tx(board,db=>{
848
+ if(!runCurrent(db,id,runId))return; // replaced once launched: its session is the ended run's, closed as one
771
849
  setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
772
850
  event(db,id,'restart_authorization_consumed',{},runId);
773
851
  });
@@ -777,12 +855,15 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
777
855
  // An issued asynchronous launch may bind later. Its original permission
778
856
  // still belongs to this attempt, never a future lost-session restart. A
779
857
  // deferred launch was never issued: its run keeps the permission for its retry.
858
+ if(error.launchSuperseded||!view(board,db=>runCurrent(db,id,runId)))throw Object.assign(error,{launchSuperseded:true});
780
859
  if(!error.launchDeferred)tx(board,db=>{
860
+ if(!runCurrent(db,id,runId))return;
781
861
  setRunSession(db,runId,{restart_authorized:false});
782
862
  event(db,id,'restart_authorization_consumed',{},runId);
783
863
  });
784
864
  if(error.launchDeferred) {
785
865
  tx(board,db=>{
866
+ if(!runCurrent(db,id,runId))return;
786
867
  const previous=currentRun(db,id)?.metadata?.supercode;
787
868
  setRunSession(db,runId,{...meta,launch_deferred:true,launch_deferred_error:error.message});
788
869
  if(previous?.launch_deferred_error!==error.message)event(db,id,'launch_deferred',{reason:error.message},runId);
@@ -799,8 +880,8 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
799
880
  // slot's session checks that session through it
800
881
  fleetRoot = board.root;
801
882
  const entry = await loadWorkflow(board.root);
802
- const fleet = observeFleet();
803
- const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
883
+ const fleet = await observeFleet();
884
+ const before = getTaskNow(board, id), checked = await checkWorkspace(board, before);
804
885
  const moved = tx(board, (db) => {
805
886
  assertWorkspaceOwner(db, before, checked);
806
887
  if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
@@ -817,9 +898,9 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
817
898
  const lane = profile ? lanes[profile] : null;
818
899
  if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
819
900
  if (!lane) return null;
820
- try { return launchRun(board,entry,id,lane,fleet,settings); }
901
+ try { return await launchRun(board,entry,id,lane,fleet,settings); }
821
902
  catch (error) {
822
- if (monitorsRuns(entry) && !error.launchPending) tx(board,db=>surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message,origin:originOf(error)}));
903
+ if (monitorsRuns(entry) && !error.launchPending && !error.launchSuperseded) launchFailed(board, entry, id, error, { error: error.message, origin: originOf(error) });
823
904
  throw error;
824
905
  }
825
906
  }
@@ -834,11 +915,11 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
834
915
  // The programs a harness pane runs in its foreground: Claude Code's, Codex's, and node (the daemon's Codex pane host,
835
916
  // a harness installed through npm). A pane running one of these runs a harness session, read or not.
836
917
  const HARNESS_COMMANDS = new Set(['claude', 'claude.exe', 'codex', 'codex.exe', 'node']);
837
- function turnAtClose(session, pane = null) {
918
+ async function turnAtClose(session, pane = null) {
838
919
  const machine = session.machine ?? localMachine();
839
920
  let activity;
840
921
  if (pane?.pid) {
841
- const r = doorOn(machine, 'harness.v1.sessions.activity_under', { pids: [pane.pid] }, { timeout: NATIVE_DISCOVERY_MS });
922
+ const r = await doorOn(machine, 'harness.v1.sessions.activity_under', { pids: [pane.pid] }, { timeout: NATIVE_DISCOVERY_MS });
842
923
  const page = r.code === 0 ? lastJson(r.stdout) : null;
843
924
  if (!Array.isArray(page?.activities)) return `the session in pane ${session.pane} could not be read just now (${(r.stderr || r.stdout).trim().slice(-160)})`;
844
925
  activity = page.activities.find((item) => item.pid === pane.pid)?.activity ?? null;
@@ -852,7 +933,7 @@ function turnAtClose(session, pane = null) {
852
933
  return `pane ${session.pane} names no process to read`;
853
934
  } else {
854
935
  if (!session.session_id) return 'its session is not known yet';
855
- try { activity = nativeReading(discoverNative(machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity ?? null; }
936
+ try { activity = nativeReading(await discoverNative(machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity ?? null; }
856
937
  catch (error) { return `its session could not be read just now (${String(error.message).slice(0, 160)})`; }
857
938
  if (!activity) return `its session ${addressOf(session)} is not in its machine's discovery`;
858
939
  }
@@ -862,7 +943,7 @@ function turnAtClose(session, pane = null) {
862
943
  return `its session ${who} is in a turn (${activity.turn ?? 'unknown'}); it closes once the turn ends`;
863
944
  }
864
945
 
865
- export function closeRequested(board, report, { taskId = null, caller = null, ttl = null, fleet = null } = {}) {
946
+ export async function closeRequested(board, report, { taskId = null, caller = null, ttl = null, fleet = null } = {}) {
866
947
  const rows = view(board, (db) => db.prepare(`SELECT id, task_id, ended_at, metadata FROM task_runs
867
948
  WHERE metadata IS NOT NULL ${taskId ? 'AND task_id = ?' : ''}`).all(...(taskId ? [taskId] : [])));
868
949
  // Repair pre-transfer history once: only identities with an older unended reference need a write.
@@ -948,17 +1029,17 @@ export function closeRequested(board, report, { taskId = null, caller = null, tt
948
1029
  try {
949
1030
  const machine = session.machine ?? localMachine();
950
1031
  if (session.surface === 'native' && !session.pane) {
951
- const reading = nativeReading(nativeSessions(machine, session.harness, session.session_id), session);
1032
+ const reading = nativeReading(await readNative(machine, session.harness, session.session_id), session);
952
1033
  if (reading.alive == null) throw new Error(`close deferred: native liveness unknown for ${addressOf(session)}`);
953
1034
  if (reading.alive) {
954
1035
  const protectedNow = active.some(r => addressOf(json(r.metadata, {})?.supercode) === addressOf(session));
955
1036
  if (protectedNow) throw new Error('close deferred: native session is owned by an unended run');
956
- if (row.ended_at) { const inTurn = turnAtClose(session); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
957
- stopSession(board, session);
1037
+ if (row.ended_at) { const inTurn = await turnAtClose(session); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
1038
+ await stopSession(board, session);
958
1039
  }
959
1040
  } else {
960
1041
  if (!panes.has(machine)) {
961
- const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
1042
+ const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
962
1043
  const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
963
1044
  panes.set(machine, Array.isArray(list) ? { live: new Map(list.filter(p => !p.dead).map((p) => [p.pane, { pid: p.pid ?? null, command: p.activeCommand ?? null }])) } : { error: `cannot read panes on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}` });
964
1045
  }
@@ -970,8 +1051,8 @@ export function closeRequested(board, report, { taskId = null, caller = null, tt
970
1051
  if (protectedNow) throw new Error(`close deferred: pane ${session.pane} is owned by an unended run`);
971
1052
  // An ended run's session is closed only between its turns, read last, after the claim and the pane list (a pane
972
1053
  // already gone is settled above with no reading; t_f34710bc's coder was cut mid-proof at 15:01:13Z, 2026-10-04).
973
- if (row.ended_at) { const inTurn = turnAtClose(session, observed.live.get(session.pane)); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
974
- stopSession(board, session); observed.live.delete(session.pane);
1054
+ if (row.ended_at) { const inTurn = await turnAtClose(session, observed.live.get(session.pane)); if (inTurn) throw new Error(`close deferred: ${inTurn}`); }
1055
+ await stopSession(board, session); observed.live.delete(session.pane);
975
1056
  }
976
1057
  }
977
1058
  } catch (e) {
@@ -1015,7 +1096,7 @@ export function closeRequested(board, report, { taskId = null, caller = null, tt
1015
1096
 
1016
1097
  /** Quiet time is independent of dispatch cadence and the harness's busy label.
1017
1098
  * Keep bounded readings on the run so an ordinary quiet stretch can be reviewed later. */
1018
- function observeIdle(board, fleet, settings, report, entry, effects) {
1099
+ async function observeIdle(board, fleet, settings, report, entry, effects) {
1019
1100
  if (!fleet.ok) return;
1020
1101
  // The published fleet says whether a session runs and its turn, not when it last wrote: that is the session's own
1021
1102
  // record, read from its machine's discovery (once a round, only when a card needs it).
@@ -1027,7 +1108,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
1027
1108
  };
1028
1109
  const managers = [].concat(entry.params.managers ?? []).filter(Boolean);
1029
1110
  const running = board.db.prepare(entry.workflow.run ? "SELECT id FROM tasks WHERE current_run_id IS NOT NULL" : "SELECT id FROM tasks WHERE status = 'running'").all();
1030
- for (const { id } of running) { const session = currentRun(board.db, id)?.metadata?.supercode; readBefore(fleet, session); if (session?.session_id) updatedAt(session); }
1111
+ for (const { id } of running) { const session = currentRun(board.db, id)?.metadata?.supercode; await readBefore(fleet, session); if (session?.session_id) updatedAt(session); }
1031
1112
  for (const { id } of running) {
1032
1113
  let staged = [];
1033
1114
  txRead(board, (db) => {
@@ -1070,8 +1151,8 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
1070
1151
  const held = live?.turn === 'needs_input' ? 'waiting_on_user' : live?.turn === 'working' && lastAt > 0 && observedAt - lastAt >= settings.idleQuiet ? 'working_silent' : null;
1071
1152
  if (held) {
1072
1153
  if (session.idle_observation?.decision === held && session.idle_observation.last_message_at_ms === reading.last_message_at_ms) return;
1073
- let screen = null;
1074
- if (session.pane) { const r = doorOn(session.machine ?? localMachine(), 'panes.capture', { pane: session.pane, lines: 25 }, { timeout: 20_000 }); screen = r.code === 0 ? String(json(r.stdout.trim(), {})?.text ?? '').trim().split('\n').slice(-25).join('\n') : null; }
1154
+ // (its screen is not read: the call here named `panes.capture`, no machine door, and never answered)
1155
+ const screen = null;
1075
1156
  const observation = retain(held, { screen });
1076
1157
  const why = held === 'waiting_on_user' ? 'is waiting on its user (a question or a permission prompt)' : `has reported working without writing anything for ${Math.round((observedAt - lastAt) / 60)} min`;
1077
1158
  const text = `${id}: its session ${addressOf(session)} ${why}; nothing it is sent reaches it until that is answered.${screen ? `\nIts screen:\n${screen}` : ''}`;
@@ -1130,7 +1211,7 @@ export async function sessionNotice(board, e) {
1130
1211
  }
1131
1212
  const address = e.address ?? addressOf(prior);
1132
1213
  if (!address) throw new Error('lifecycle notice awaits the owned launch session identity');
1133
- const filed = fileMail(board, address, e.text, effectMessageKey(e));
1214
+ const filed = await fileMail(board, address, e.text, effectMessageKey(e));
1134
1215
  tx(board, (db) => {
1135
1216
  const session = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(e.runId)?.metadata, {})?.supercode;
1136
1217
  if (session?.lifecycle_notice_pending === e.durableId) setRunSession(db, e.runId, { lifecycle_notice_pending: null });
@@ -1147,7 +1228,7 @@ export async function sessionEffect(board, e) {
1147
1228
  const address = addressOf(session);
1148
1229
  if (!address) throw new Error('actor session identity unavailable');
1149
1230
  // filed in the actor's mailbox, which delivers it
1150
- const filed = fileMail(board, address, e.text, effectMessageKey(e));
1231
+ const filed = await fileMail(board, address, e.text, effectMessageKey(e));
1151
1232
  if (filed.new) tx(board, db => event(db, e.id, 'actor_notice_filed', { notice: e.durableId, address, message_id: filed.message_id }, e.runId));
1152
1233
  return true;
1153
1234
  }
@@ -1178,7 +1259,7 @@ export async function mergeReviewed(board, e) {
1178
1259
  for (const p of prs) {
1179
1260
  if (Date.now() - began > MERGE_BUDGET_MS) throw new Error(`the merge attempt spent its ${MERGE_BUDGET_MS / 1000} s; the rest is merged next attempt`);
1180
1261
  let answer;
1181
- try { answer = mergeReviewedPullRequest(p.pr.url, p.commit); } catch (error) {
1262
+ try { answer = await mergeReviewedPullRequest(p.pr.url, p.commit); } catch (error) {
1182
1263
  const reason = String(error.message).slice(-300);
1183
1264
  tx(board, (db) => event(db, e.id, 'merge_pending', { pr: p.pr.url, reviewed: p.commit, reason }));
1184
1265
  throw new Error(`merging ${p.pr.url} at ${p.commit}: ${reason}`);
@@ -1246,11 +1327,18 @@ const PR_URL = /https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+/i;
1246
1327
  /**
1247
1328
  * This machine's memory pressure: on Linux as Hermes reads it (MemAvailable); on macOS the kernel's own level
1248
1329
  * (kern.memorystatus_vm_pressure_level: 1 normal, 2 warning, 4 critical); `unknown` elsewhere, which restricts nothing.
1330
+ * Read once per round (readMemoryPressure, awaited at the round's start) and held for that round's caps.
1249
1331
  */
1250
- function memoryPressure() {
1332
+ let pressureReading = 'unknown';
1333
+ const memoryPressure = () => pressureReading;
1334
+ export async function readMemoryPressure() {
1335
+ pressureReading = await pressureNow();
1336
+ return pressureReading;
1337
+ }
1338
+ async function pressureNow() {
1251
1339
  if (process.platform === 'darwin') {
1252
- const r = spawnSync('sysctl', ['-n', 'kern.memorystatus_vm_pressure_level'], { encoding: 'utf8', timeout: 5_000 });
1253
- const level = Number(String(r.stdout ?? '').trim());
1340
+ const stdout = await new Promise((resolve) => execFile('sysctl', ['-n', 'kern.memorystatus_vm_pressure_level'], { encoding: 'utf8', timeout: 5_000 }, (_, out) => resolve(out ?? '')));
1341
+ const level = Number(String(stdout).trim());
1254
1342
  return level >= 4 ? 'critical' : level >= 2 ? 'elevated' : level === 1 ? 'ok' : 'unknown';
1255
1343
  }
1256
1344
  try {
@@ -1305,8 +1393,8 @@ function launchReading(fleet, session) {
1305
1393
  }
1306
1394
 
1307
1395
  /** How a keyed launch ended, as its machine recorded it; read only where the machine has the door (supervise). */
1308
- function launchReceipt(board, session) {
1309
- const r = doorOn(session.machine, 'harness.v1.launches.get',{key:session.launch_key},
1396
+ async function launchReceipt(board, session) {
1397
+ const r = await doorOn(session.machine, 'harness.v1.launches.get',{key:session.launch_key},
1310
1398
  { timeout: 30_000, as: boardAddress(board.label) });
1311
1399
  if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1312
1400
  if (r.stdout.trim() === 'null') return null;
@@ -1336,7 +1424,7 @@ function bindLaunch(board, row, session, pane, sid) {
1336
1424
  }
1337
1425
 
1338
1426
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
1339
- export function reconcileCallerLaunch(board, sid) {
1427
+ export async function reconcileCallerLaunch(board, sid) {
1340
1428
  fleetRoot = board.root;
1341
1429
  if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
1342
1430
  WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
@@ -1350,7 +1438,7 @@ export function reconcileCallerLaunch(board, sid) {
1350
1438
  return s?.machine === machine && s.harness === harness && (!s.session_id || s.session_id === sid);
1351
1439
  });
1352
1440
  if (!candidates.length) return;
1353
- const pane = process.env.SUPERCODE_TEAMS_PANE || observeFleet().sessions?.find((s) => s.address === `sc:${machine}:${harness}:${sid}`)?.pane;
1441
+ const pane = process.env.SUPERCODE_TEAMS_PANE || (await observeFleet()).sessions?.find((s) => s.address === `sc:${machine}:${harness}:${sid}`)?.pane;
1354
1442
  if (!pane) return;
1355
1443
  // the caller's own pane (its environment, or this machine's reading) binds the run whose launch recorded that pane
1356
1444
  for (const row of candidates) {
@@ -1362,7 +1450,7 @@ export function reconcileCallerLaunch(board, sid) {
1362
1450
  /** The board's agent records: each run kept in the agent store, closed cards' assignments ended, declarations and ends
1363
1451
  * handed to this machine's daemon, and usage read from each machine's own doors. No Teams call: the daemon declares and
1364
1452
  * ends agents and binds their sessions (D137). It runs in the delivery pass (`deliver`), never in a dispatch round. */
1365
- function synchronizeAgents(board, report) {
1453
+ async function synchronizeAgents(board, report) {
1366
1454
  // A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
1367
1455
  // A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
1368
1456
  const agentRuns=[...new Map(view(board,db=>db.prepare("SELECT id,task_id,metadata,ended_at FROM task_runs WHERE json_extract(metadata,'$.supercode.agent_id') IS NOT NULL ORDER BY id").all())
@@ -1395,9 +1483,9 @@ function synchronizeAgents(board, report) {
1395
1483
  if(!run?.ended_at||read>=5)continue;
1396
1484
  const [harness,...rest]=row.session_ref.split(':'),machine=row.machine&&normalMachine(row.machine)!==localMachine()?row.machine:null;
1397
1485
  // the usage door loads the session by its whole locator, which discovery names
1398
- const sessionId=rest.join(':'),found=runtimeDoor(machine,'harness.v1.sessions.discover',{query:sessionId,harnesses:[harness],limit:200},{timeout:30_000});
1486
+ const sessionId=rest.join(':'),found=await runtimeDoor(machine,'harness.v1.sessions.discover',{query:sessionId,harnesses:[harness],limit:200},{timeout:30_000});
1399
1487
  const locator=found.code===0?json(found.stdout.trim(),{})?.sessions?.find(item=>item.locator?.harness===harness&&item.locator?.session_id===sessionId)?.locator:null;
1400
- const reply=locator?runtimeDoor(machine,'harness.v1.sessions.usage',{locator},{timeout:30_000}):{code:1};read++;
1488
+ const reply=locator?await runtimeDoor(machine,'harness.v1.sessions.usage',{locator},{timeout:30_000}):{code:1};read++;
1401
1489
  if(reply.code===0)recordRunUsage(board.root,org,row.session_ref,json(reply.stdout.trim(),{})?.usage??[]);
1402
1490
  // a session unreadable a day after its run ended is recorded as having no usage record, so it stops retrying
1403
1491
  else if(now()-Number(run.ended_at)>86400)recordRunUsage(board.root,org,row.session_ref,[]);
@@ -1409,11 +1497,11 @@ function synchronizeAgents(board, report) {
1409
1497
  if(task&&(task.archived||['done','cancelled'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
1410
1498
  }
1411
1499
  // What a creating or closing verb could not hand on (no daemon running then) is handed on here.
1412
- try{for(const pending of handOffAgents(board.root))report.queued.push([pending.agent,pending.error]);}
1500
+ try{for(const pending of await handOffAgents(board.root))report.queued.push([pending.agent,pending.error]);}
1413
1501
  catch(error){report.queued.push(['agents',`agent hand-off waits: ${error.message}`]);}
1414
1502
  }
1415
1503
 
1416
- function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
1504
+ async function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
1417
1505
  const deferred=view(board,db=>db.prepare("SELECT t.id,r.id run_id,COALESCE(json_extract(r.metadata,'$.supercode.launched_at'),r.started_at) launched_at FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
1418
1506
  // A deferred launch was never issued (a reading or door was not yet available): it is the start its manager, creator
1419
1507
  // or workflow asked for, carried out within the launch window; supervise surfaces it once when the window ends. The
@@ -1431,13 +1519,15 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1431
1519
  const why=launcherCheck(board.db,task,lane,fleet);if(why){report.queued.push([id,why]);continue;}
1432
1520
  const claimed=tx(board,db=>{if(!stillDeferred(db))return false;setRunSession(db,run_id,{launch_deferred:false});return true;});
1433
1521
  if(!claimed){report.queued.push([id,'its launch was taken by another round']);continue;}
1434
- const session=launchRun(board,entry,id,lane,fleet,settings);
1522
+ const session=await launchRun(board,entry,id,lane,fleet,settings);
1435
1523
  if(session)report.started.push([id,session.address??session.session_id,session.mode]);
1436
1524
  // nothing to launch now (its status runs nothing yet): the run waits deferred, as before the claim
1437
1525
  else tx(board,db=>{const run=db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(run_id);const saved=json(run?.metadata,{})?.supercode??{};if(!saved.launch_key&&!saved.pid)setRunSession(db,run_id,{launch_deferred:true});});
1438
1526
  }catch(error){
1439
- if(error.launchPending){report.queued.push([id,error.message]);continue;}
1440
- const moved=tx(board,db=>monitorsRuns(entry)?(surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message,origin:originOf(error)}),{effects:[]}):fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
1527
+ if(error.launchPending||error.launchSuperseded){report.queued.push([id,error.message]);continue;}
1528
+ const moved=launchFailed(board,entry,id,error,monitorsRuns(entry)?{error:error.message,origin:originOf(error)}:{error:error.message});
1529
+ if(!moved){report.queued.push([id,`run ${error.launchRun} was replaced before its launch failed: ${error.message}`]);continue;}
1530
+ effects.push(...moved.effects);
1441
1531
  report.failed.push([id,error.message]);
1442
1532
  }
1443
1533
  }
@@ -1474,15 +1564,15 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1474
1564
  }
1475
1565
  }
1476
1566
 
1477
- function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'worker', meta = {} } = {}) {
1567
+ async function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'worker', meta = {} } = {}) {
1478
1568
  const machine = normalMachine(task.machine);
1479
1569
  // Whether a board session opens a terminal window on its machine is the workflow's choice, stated on every launch;
1480
1570
  // no machine default decides it.
1481
1571
  const window = workflowFor(board.root).params.session_window;
1482
1572
  if (typeof window !== 'boolean') throw unavailable('the workflow declares no params.session_window (true or false): whether a board session opens a window on its machine is the workflow\'s choice');
1483
1573
  {
1484
- const made = resolveLocalWorkspace(board, task);
1485
- tx(board, (db) => {
1574
+ const made = await resolveLocalWorkspace(board, task);
1575
+ runTx(board, task.id, runId, (db) => {
1486
1576
  db.prepare('UPDATE tasks SET workspace_path=?, branch_name=COALESCE(branch_name,?) WHERE id=?').run(made, task.workspace_kind === 'worktree' ? `wt/${task.id}` : null, task.id);
1487
1577
  });
1488
1578
  task = { ...task, workspace_path: made };
@@ -1500,7 +1590,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1500
1590
  // a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
1501
1591
  // session starts (never two processes on one session)
1502
1592
  // Every old pane in the slot must be settled, including a failed launch with no session id.
1503
- closeRequested(board, { undelivered: [] }, { taskId: task.id });
1593
+ await closeRequested(board, { undelivered: [] }, { taskId: task.id });
1504
1594
  const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
1505
1595
  .map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
1506
1596
  .find((r) => (r.session?.pane || r.session?.launch_key || r.session?.surface === 'native') && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at);
@@ -1510,7 +1600,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1510
1600
  // the round's own fleet reading: a launch never reads the fleet again
1511
1601
  const fresh = fleet;
1512
1602
  if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
1513
- const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1603
+ const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1514
1604
  const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
1515
1605
  if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1516
1606
  const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
@@ -1519,7 +1609,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1519
1609
  }
1520
1610
  if (prev?.close_requested && !prev.closed_at) {
1521
1611
  const report = { undelivered: [] };
1522
- closeRequested(board, report, { taskId: task.id });
1612
+ await closeRequested(board, report, { taskId: task.id });
1523
1613
  prev = view(board, (db) => lastSession(db, task.id, slot));
1524
1614
  // a pane that could not be closed still holds the session: no second process is started on it
1525
1615
  if (prev?.close_requested && !prev.closed_at) throw unavailable(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
@@ -1529,12 +1619,20 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1529
1619
  // launch (Claude Code's) is bound with it, one that names itself (Codex) is bound from a later round's fleet reading.
1530
1620
  // The pane id is chosen from the launch key and recorded before open runs, so a launch whose answer is lost (a
1531
1621
  // timeout, a closed stream) is still found by its pane in a later round's fleet reading, and by its session id.
1532
- const openOwned = (key, sid, mode, args) => {
1622
+ const openOwned = async (key, sid, mode, args) => {
1533
1623
  const chosen = `p_${createHash('sha256').update(key).digest('hex').slice(0, 16)}`;
1534
- tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
1535
- session_id: sid, mode, launch_key: key, pane: chosen, launch_pending: true, launched_at: now() }));
1536
- const result = sc([...args.slice(0, args.indexOf('--key')), '--pane', chosen, ...args.slice(args.indexOf('--key'))], { timeout: 180_000, as: boardAddress(board.label) });
1624
+ tx(board, (db) => {
1625
+ if (!runCurrent(db, task.id, runId)) throw Object.assign(new Error(`run ${runId} of ${task.id} ended or was replaced while its launch was prepared; nothing was started`), { launchSuperseded: true });
1626
+ setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
1627
+ session_id: sid, mode, launch_key: key, pane: chosen, launch_pending: true, launched_at: now() });
1628
+ });
1629
+ const result = await sc([...args.slice(0, args.indexOf('--key')), '--pane', chosen, ...args.slice(args.indexOf('--key'))], { timeout: 180_000, as: boardAddress(board.label) });
1537
1630
  const pane = lastJson(result.stdout)?.pane ?? chosen;
1631
+ // the run ended while its pane opened: the pane it opened is closed, never left working for an ended run
1632
+ if (!view(board, (db) => runCurrent(db, task.id, runId))) {
1633
+ try { await stopSession(board, { surface: 'pane', machine, harness, pane, session_id: sid, launch_key: key }); } catch { /* the slot's next launch settles it */ }
1634
+ throw Object.assign(new Error(`run ${runId} of ${task.id} ended while its pane opened; pane ${pane} was closed`), { launchSuperseded: true });
1635
+ }
1538
1636
  if (pane !== chosen) tx(board, (db) => {
1539
1637
  const before = json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(runId)?.metadata, {})?.supercode ?? {};
1540
1638
  setRunSession(db, runId, { ...before, pane });
@@ -1546,6 +1644,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1546
1644
  const reason = result.code === 0 ? `launch ${key} opened pane ${pane}; its ${harness} session is bound from the fleet reading` :
1547
1645
  `launch ${key} (pane ${pane}) gave no answer; it is bound from the fleet reading if it opened: ${(result.stderr || result.stdout).trim().slice(-300)}`;
1548
1646
  tx(board, (db) => {
1647
+ if (!runCurrent(db, task.id, runId)) return;
1549
1648
  setRunSession(db, runId, { launch_pending: true, launch_error: reason });
1550
1649
  event(db, task.id, 'session_launch_pending', { key, pane, reason }, runId);
1551
1650
  });
@@ -1557,34 +1656,38 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1557
1656
  // session it did not launch, left running at its run's end) is its owner's while it runs: it is never resumed beside
1558
1657
  // itself, and the launch waits for the owner to end it.
1559
1658
  if (prev?.surface === 'native' && !prev.pane && (!prev.close_confirmed_at || prev.released)) {
1560
- const reading = nativeReading(nativeSessions(prev.machine, prev.harness, prev.session_id), prev);
1659
+ const reading = nativeReading(await readNative(prev.machine, prev.harness, prev.session_id), prev);
1561
1660
  if (reading.alive == null) throw unavailable(`cannot establish native continuation liveness for ${addressOf(prev)}`);
1562
1661
  fleet.native ??= new Map(); fleet.native.set(addressOf(prev), reading);
1563
1662
  if (reading.alive && prev.released) throw unavailable(`released session ${addressOf(prev)} still runs; the launch waits for its owner to end it`);
1564
1663
  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`);
1565
1664
  }
1566
1665
  if (prev?.forgotten) {
1567
- if (!prev.close_confirmed_at && prev.pane && panePresent(prev.machine, prev.pane)) throw new Error(`forgotten session ${prev.session_id} still has live pane ${prev.pane}; close it before replacement`);
1666
+ if (!prev.close_confirmed_at && prev.pane && await panePresent(prev.machine, prev.pane)) throw new Error(`forgotten session ${prev.session_id} still has live pane ${prev.pane}; close it before replacement`);
1568
1667
  replaced = { session_id: prev.session_id, why: 'the workflow let it go' };
1569
1668
  }
1570
1669
  else if (prev && (prev.harness ?? 'claude-code') !== harness) {
1571
1670
  // the card was reassigned to a lane of another harness: that harness starts it fresh (the card's text and thread
1572
1671
  // are its context), and the last session, if still open, is closed so two harnesses never work one card
1573
- if (prev.pane && !prev.closed_at && !adoptedOnly(prev)) stopSession(board, prev); // an adopted session is released, never closed
1672
+ if (prev.pane && !prev.closed_at && !adoptedOnly(prev)) await stopSession(board, prev); // an adopted session is released, never closed
1574
1673
  replaced = { session_id: prev.session_id, why: `the card's lane runs ${harness}, not ${prev.harness ?? 'claude-code'}`, letGo: true };
1575
1674
  } else if (prev && prev.machine === machine) {
1576
1675
  // a session the board closed, or one whose close its own command asked for (the next tick's sweep closes that
1577
1676
  // run's pane), is not kept, whatever a fleet reading taken before the close still lists
1578
- const paneLive = prev.pane ? panePresent(prev.machine, prev.pane) : false;
1677
+ const paneLive = prev.pane ? await panePresent(prev.machine, prev.pane) : false;
1579
1678
  if (paneLive && (prev.closed_at || prev.close_requested || prev.forgotten)) throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
1580
1679
  if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
1581
1680
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
1582
1681
  const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, monitor_held: _mh, ...kept } = prev;
1583
1682
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
1584
- // a kept session is told what this run is for (its card came back to it, with what happened since)
1585
- tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
1683
+ // a kept session is told what this run is for (its card came back to it, with what happened since), only while
1684
+ // this run is still the card's current one (its pane and native reads were awaited)
1685
+ tx(board, (db) => {
1686
+ if (!runCurrent(db, task.id, runId)) throw Object.assign(new Error(`run ${runId} of ${task.id} ended or was replaced while its session was read; nothing was told to it`), { launchSuperseded: true });
1687
+ setRunSession(db, runId, { ...session, ...meta });
1688
+ });
1586
1689
  // filed in its mailbox, which delivers it
1587
- if (input) fileMail(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`);
1690
+ if (input) { assertRunCurrent(board, task.id, runId); await fileMail(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`); }
1588
1691
  } else if (prev.resumable !== false) {
1589
1692
  const authorization = view(board, db => currentRun(db,task.id)?.metadata?.supercode?.restart_authorized);
1590
1693
  const released = view(board,db=>{
@@ -1601,7 +1704,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1601
1704
  // a session is resumed in its own harness, whatever the lane's is now
1602
1705
  const its = HARNESSES[prev.harness ?? 'claude-code'] ?? HARNESSES['claude-code'];
1603
1706
  const launchKey = `board-${task.id}-r${runId}-resume`;
1604
- session = openOwned(launchKey, prev.session_id, 'resume', ['open', '--new', its.program, ...base.slice(3),
1707
+ session = await openOwned(launchKey, prev.session_id, 'resume', ['open', '--new', its.program, ...base.slice(3),
1605
1708
  '--key', launchKey, '--input', told, '--', ...its.resume(prev.session_id), ...tail]);
1606
1709
  } else replaced = { session_id: prev.session_id, why: 'its resume never came up' };
1607
1710
  } else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
@@ -1615,7 +1718,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1615
1718
  if (!session) {
1616
1719
  const sid = how.namesItself ? null : randomUUID();
1617
1720
  const launchKey = `board-${task.id}-r${runId}-${(sid ?? randomUUID()).slice(0,8)}`;
1618
- session = openOwned(launchKey, sid, 'fresh', [...base, '--key', launchKey,
1721
+ session = await openOwned(launchKey, sid, 'fresh', [...base, '--key', launchKey,
1619
1722
  '--input', input ?? `Card ${task.id}: ${task.title}`, '--', ...(sid ? how.fresh(sid) : how.fresh()), ...tail]);
1620
1723
  if (replaced) session.replaces = replaced.session_id;
1621
1724
  }
@@ -1649,24 +1752,25 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
1649
1752
  }
1650
1753
 
1651
1754
  /** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
1652
- function panePresent(machine, pane) {
1653
- const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1755
+ async function panePresent(machine, pane) {
1756
+ const r = await doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
1654
1757
  const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
1655
1758
  if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
1656
1759
  return panes.some((p) => p.pane === pane && !p.dead);
1657
1760
  }
1658
1761
 
1659
1762
  /** Whether a pane just launched is still there after the harness had a moment to start. */
1660
- function paneSurvives(machine, pane) {
1661
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 6000);
1662
- return panePresent(machine, pane);
1763
+ async function paneSurvives(machine, pane) {
1764
+ // a timer, never a blocking wait: the owner answers verbs while the harness starts
1765
+ await new Promise((resolve) => setTimeout(resolve, 6000));
1766
+ return await panePresent(machine, pane);
1663
1767
  }
1664
1768
 
1665
1769
  /** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
1666
- function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
1667
- const workspace = resolveLocalWorkspace(board, task);
1770
+ async function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
1771
+ const workspace = await resolveLocalWorkspace(board, task);
1668
1772
  if(meta.agent_profile_intent)meta.agent_profile_intent.worker={...meta.agent_profile_intent.worker,harness:lane.harness,model:task.model_override??lane.model??null,cwd:workspace};
1669
- tx(board, (db) => db.prepare('UPDATE tasks SET workspace_path=?,branch_name=COALESCE(branch_name,?) WHERE id=?').run(workspace, task.workspace_kind === 'worktree' ? `wt/${task.id}` : null, task.id));
1773
+ runTx(board, task.id, runId, (db) => db.prepare('UPDATE tasks SET workspace_path=?,branch_name=COALESCE(branch_name,?) WHERE id=?').run(workspace, task.workspace_kind === 'worktree' ? `wt/${task.id}` : null, task.id));
1670
1774
  // the card's own skills and the ones its status's run names (a reviewer's, for one)
1671
1775
  const skills = [...new Set([...task.skills, ...runSkills])];
1672
1776
  const args = ['-p', lane.name, '--cli', '--accept-hooks', ...skills.flatMap((s) => ['--skills', s]),
@@ -1694,12 +1798,19 @@ function launchHeadless(board, task, runId, lane, settings, { prompt = null, ski
1694
1798
  });
1695
1799
  child.unref();
1696
1800
  const session = { surface: 'headless', pid: child.pid, machine: localMachine(), mode: 'fresh', launched_at: now(), exit_file: exitFile, launch_secret: createHash('sha256').update(launchSecret).digest('hex'), ...meta };
1697
- tx(board, (db) => {
1698
- db.prepare('UPDATE tasks SET worker_pid = ? WHERE id = ?').run(child.pid, task.id);
1801
+ // recorded against this run only while it is still the card's current one; a run ended meanwhile gets no worker
1802
+ const recorded = tx(board, (db) => {
1803
+ if (!runCurrent(db, task.id, runId)) return false;
1804
+ db.prepare('UPDATE tasks SET worker_pid = ? WHERE id = ? AND current_run_id = ?').run(child.pid, task.id, runId);
1699
1805
  db.prepare('UPDATE task_runs SET worker_pid = ? WHERE id = ?').run(child.pid, runId);
1700
1806
  setRunSession(db, runId, session);
1701
1807
  event(db, task.id, 'spawned', { pid: child.pid }, runId);
1808
+ return true;
1702
1809
  });
1810
+ if (!recorded) {
1811
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { try { process.kill(child.pid, 'SIGTERM'); } catch { /* already gone */ } }
1812
+ throw Object.assign(new Error(`run ${runId} of ${task.id} ended while its worker started; the worker was stopped`), { launchSuperseded: true });
1813
+ }
1703
1814
  return session;
1704
1815
  }
1705
1816
 
@@ -1714,9 +1825,9 @@ function rotateLog(file, max, backups) {
1714
1825
  }
1715
1826
 
1716
1827
  /** A headless card's directory on this machine, made as Hermes makes it (scratch, dir, or a git worktree). */
1717
- function resolveLocalWorkspace(board, task) {
1828
+ async function resolveLocalWorkspace(board, task) {
1718
1829
  const root = board.root;
1719
- if (task.workspace_kind !== 'scratch') return resolveWorkspace(task, true).path;
1830
+ if (task.workspace_kind !== 'scratch') return (await resolveWorkspace(task, true)).path;
1720
1831
  const dir = task.workspace_path || (board.slug === 'default' ? join(root, 'kanban', 'workspaces', task.id) : join(root, 'kanban', 'boards', board.slug, 'workspaces', task.id));
1721
1832
  mkdirSync(dir, { recursive: true });
1722
1833
  return dir;
@@ -1760,7 +1871,7 @@ const latestAnswer = (db, id) => db.prepare(`SELECT id, kind, payload FROM task_
1760
1871
  * of two rounds that read one request, one binds it and the other leaves it; the bind and the failure are each written
1761
1872
  * only while that claim is the card's latest.
1762
1873
  */
1763
- export function completeAdoptions(board, fleet, settings, report, lanes = null) {
1874
+ export async function completeAdoptions(board, fleet, settings, report, lanes = null) {
1764
1875
  const pending = view(board, db => db.prepare(`SELECT e.id, e.task_id, e.kind, e.payload, e.created_at FROM task_events e
1765
1876
  WHERE ((e.kind = 'dispatch_held' AND json_extract(e.payload, '$.reason') = 'adoption requested') OR e.kind = 'adoption_claimed')
1766
1877
  AND e.id = (SELECT MAX(id) FROM task_events l WHERE l.task_id = e.task_id AND l.kind IN ${ADOPTION_ANSWERS})`).all());
@@ -1776,7 +1887,7 @@ export function completeAdoptions(board, fleet, settings, report, lanes = null)
1776
1887
  const asked = json(request.payload, {}) ?? {};
1777
1888
  const progress = {};
1778
1889
  try {
1779
- const got = adoptSession(board, row.task_id, asked.session, { request: request.id, open: row.id, progress, workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
1890
+ const got = await adoptSession(board, row.task_id, asked.session, { request: request.id, open: row.id, progress, workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
1780
1891
  (report.adopted ??= []).push([row.task_id, got.address]);
1781
1892
  } catch (error) {
1782
1893
  const message = String(error.message).slice(-400);
@@ -1807,11 +1918,11 @@ function adoptionOpenTo(db, id, request, open) {
1807
1918
  }
1808
1919
 
1809
1920
  /** A session in no pane (a native process its machine's harness discovery holds as running), by its full address. */
1810
- function nativeListing(who) {
1921
+ async function nativeListing(who) {
1811
1922
  const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(who));
1812
1923
  if (!m || !HARNESSES[m[2]]) return null;
1813
1924
  const [, host, harness, session_id] = m;
1814
- try { return nativeReading(nativeSessions(host, harness, session_id), { session_id, harness }).alive === true ? { address: who, machine: normalMachine(host), harness, session_id, pane: null, name: null } : null; }
1925
+ try { return nativeReading(await readNative(host, harness, session_id), { session_id, harness }).alive === true ? { address: who, machine: normalMachine(host), harness, session_id, pane: null, name: null } : null; }
1815
1926
  catch { return null; }
1816
1927
  }
1817
1928
 
@@ -1831,27 +1942,27 @@ function adoptedRole(db, board, entry, task, run) {
1831
1942
  return runSpec(entry, t?.target ? String(templateValue(t.target, scope)) : status, task)?.role ?? null;
1832
1943
  }
1833
1944
 
1834
- function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1835
- fleet ??= observeFleet();
1945
+ async function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1946
+ fleet ??= await observeFleet();
1836
1947
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1837
- const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? nativeListing(who);
1948
+ const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? await nativeListing(who);
1838
1949
  if (!found) throw Object.assign(new Error(`the fleet lists no live session ${who}`), { notListed: true });
1839
1950
  const [, host, harness, sessionId] = found.address.split(':');
1840
1951
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
1841
- const native = found.pane ? null : nativeReading(nativeSessions(host, harness, sessionId), { session_id: sessionId, harness });
1842
- if (found.pane ? !panePresent(host, found.pane) : native.alive !== true) throw new Error(`${found.address} has no verified live native process or pane`);
1952
+ const native = found.pane ? null : nativeReading(await readNative(host, harness, sessionId), { session_id: sessionId, harness });
1953
+ if (found.pane ? !await panePresent(host, found.pane) : native.alive !== true) throw new Error(`${found.address} has no verified live native process or pane`);
1843
1954
  const beforeRun = view(board, (db) => currentRun(db, id));
1844
1955
  const beforeSession = beforeRun?.metadata?.supercode;
1845
1956
  if (beforeRun && beforeSession?.session_id !== sessionId) throw new Error(`${id} has another active run/session; end it before adopting a different identity`);
1846
- if (beforeSession?.pane && beforeSession.pane !== found.pane && panePresent(beforeSession.machine, beforeSession.pane)) throw new Error(`${id} still has live pane ${beforeSession.pane}; close the extra pane before rebinding ${sessionId}`);
1957
+ if (beforeSession?.pane && beforeSession.pane !== found.pane && await panePresent(beforeSession.machine, beforeSession.pane)) throw new Error(`${id} still has live pane ${beforeSession.pane}; close the extra pane before rebinding ${sessionId}`);
1847
1958
 
1848
1959
  // the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
1849
1960
  let dir = workspace ?? native?.row?.cwd;
1850
1961
  if (!dir) {
1851
- dir = nativeSessions(host, harness, sessionId).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
1962
+ dir = (await readNative(host, harness, sessionId)).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
1852
1963
  }
1853
1964
  if (!dir && found.pane) {
1854
- const r = runtimeDoor(normalMachine(host)===localMachine()?null:host,'harness.v1.panes.list',{}, { timeout: 60_000 });
1965
+ const r = await runtimeDoor(normalMachine(host)===localMachine()?null:host,'harness.v1.panes.list',{}, { timeout: 60_000 });
1855
1966
  dir = (json(r.stdout.trim(), []) ?? []).find((p) => p.pane === found.pane)?.cwd ?? null;
1856
1967
  }
1857
1968
  // Rule 2: a session working a card role is that role's agent, however it started; adopted, it joins the agent as a
@@ -1875,11 +1986,13 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1875
1986
  });
1876
1987
  progress.claimed = request !== null;
1877
1988
  // The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
1878
- 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;
1989
+ const agent = lane ? await 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;
1879
1990
  // A session another agent already owns is refused before anything is recorded: the run would name one agent and the
1880
1991
  // agent store another, and every round after would trip on it.
1881
1992
  const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
1882
1993
  if (owner && owner.agent_id !== agent.identity.id) throw new Error(`${found.address} already works for agent ${owner.agent_id} (run ${owner.run_id}); it cannot be adopted as ${agent.identity.id}`);
1994
+ // the card's previous session is read before the bind decides whether it is still live
1995
+ await readBefore(fleet, view(board, (db) => lastSession(db, id)));
1883
1996
  const adopted = tx(board, (db) => {
1884
1997
  const task = getTask(db, id);
1885
1998
  if (!task) throw new Error(`no card ${id}`);
@@ -1929,7 +2042,7 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1929
2042
  }
1930
2043
 
1931
2044
  /** A manager/creator's manual native resume rebinds its existing run; this opens no process. */
1932
- function reconnectContinuedSessions(board, fleet, entry, report) {
2045
+ async function reconnectContinuedSessions(board, fleet, entry, report) {
1933
2046
  if (!monitorsRuns(entry) || !fleet.ok) return;
1934
2047
  const rows = view(board, db => db.prepare(`SELECT r.* FROM task_runs r JOIN tasks t ON t.id=r.task_id
1935
2048
  WHERE t.current_run_id IS NULL AND t.status NOT IN ('done','archived') AND r.id=(SELECT MAX(id) FROM task_runs WHERE task_id=t.id)
@@ -1942,7 +2055,7 @@ function reconnectContinuedSessions(board, fleet, entry, report) {
1942
2055
  const live = fleet.sessions?.find(s => s.address === addressOf(saved) && s.state === 'running' && s.pane);
1943
2056
  if (!live) continue;
1944
2057
  if (!panes.has(saved.machine)) {
1945
- const answer = doorOn(saved.machine,'harness.v1.panes.list',{}, {timeout:60000});
2058
+ const answer = await doorOn(saved.machine,'harness.v1.panes.list',{}, {timeout:60000});
1946
2059
  const list = answer.code === 0 ? json(answer.stdout,null) : null;
1947
2060
  panes.set(saved.machine,Array.isArray(list) ? new Set(list.filter(p=>!p.dead).map(p=>p.pane)) : null);
1948
2061
  }
@@ -1977,22 +2090,25 @@ function reconnectContinuedSessions(board, fleet, entry, report) {
1977
2090
 
1978
2091
 
1979
2092
  /**
1980
- * File one message from the board in its receiver's mailbox, and stop there (D138 step 6, D140): a local write through
1981
- * the mailbox's filing door (`supercode message file`), idempotent by the message id `key` fixes, with no network and
1982
- * nothing to try again. The mailbox delivers it: the machine daemon's watch loop hands it to its session by the
1983
- * session's own door, and carries mail for another machine there through Teams. `wake: false` leaves it for the
1984
- * session's next turn. A question the board waits on (`ask`) is answered by a reply to the board's address; a notice
1985
- * is not. Throws only when the local write failed.
2093
+ * File one message from the board in its receiver's mailbox, and stop there (D138 step 6, D140): through this machine's
2094
+ * daemon (`harness.v1.mail.file`, in this process over its held channel), which names the sender's identity itself and
2095
+ * writes it through the mailbox's filing door, idempotent by the message id `key` fixes. Nothing here waits on the
2096
+ * event loop. The mailbox delivers it: the machine daemon's watch loop hands it to its session by the session's own
2097
+ * door, and carries mail for another machine there through Teams. `wake: false` leaves it for the session's next turn.
2098
+ * A question the board waits on (`ask`) is answered by a reply to the board's address; a notice is not. Throws only
2099
+ * when the filing failed.
1986
2100
  */
1987
- function fileMail(board, to, body, key, { ask = false, wake = true, subject } = {}) {
2101
+ async function fileMail(board, to, body, key, { ask = false, wake = true, subject } = {}) {
1988
2102
  if (boardCopyOf(board)) throw copyRefusal(board, 'files no mail for sessions or managers');
1989
2103
  const request = {
1990
2104
  from: boardAddress(board.label), from_name: `${board.label} board`, to, id: key, wake, subject: String(subject ?? String(body).split('\n')[0]).slice(0, 160),
1991
2105
  body: ask ? `[A question from the ${board.label} board] ${body}` : `[Board notice from the ${board.label} board] ${body}\n\n(An automated notice from the board's dispatcher; replies to this address go nowhere.)`,
2106
+ // the board's own Teams context (the call it files in): the sender is observed in that team, never the daemon's
2107
+ context: current().facts.SUPERCODE_CONTEXT ?? null,
1992
2108
  };
1993
- const r = sc(['message', 'file'], { input: JSON.stringify(request), timeout: 30_000 });
1994
- const answer = lastJson(r.stdout);
1995
- if (answer?.code !== 0) throw new Error((answer?.text || r.stderr || `no answer from the mailbox's filing door (exit ${r.code})`).trim().slice(-300));
2109
+ const r = await runtimeDoor(null, 'harness.v1.mail.file', request, { timeout: 30_000 });
2110
+ const answer = r.code === 0 ? lastJson(r.stdout) : null;
2111
+ if (answer?.code !== 0) throw new Error((answer?.text || (lastJson(r.stderr)?.message ?? r.stderr) || 'no answer from the mailbox\'s filing door').trim().slice(-300));
1996
2112
  return { filed: true, message_id: answer.message_id, new: answer.new === true };
1997
2113
  }
1998
2114
 
@@ -2006,7 +2122,7 @@ export async function alertManagers(root, slug, text, key) {
2006
2122
  const label = boardLabel(root, slug);
2007
2123
  let told = 0;
2008
2124
  for (const to of managers) {
2009
- try { fileMail({ root, label }, to, text, key, { subject: text.split('\n')[0] }); told += 1; } catch { /* a filing that failed here is said by the next alert */ }
2125
+ try { await fileMail({ root, label }, to, text, key, { subject: text.split('\n')[0] }); told += 1; } catch { /* a filing that failed here is said by the next alert */ }
2010
2126
  }
2011
2127
  return told;
2012
2128
  }
@@ -2017,7 +2133,7 @@ export async function alertManagers(root, slug, text, key) {
2017
2133
  * waits on, so a second filing of it files nothing new. A mail inside its batch window waits for the next pass; one
2018
2134
  * whose filing failed here is reported and filed by the next pass.
2019
2135
  */
2020
- export function fileBoardMail(board, report, taskId = null) {
2136
+ export async function fileBoardMail(board, report, taskId = null) {
2021
2137
  // a message older than the workflow's notice_max_age_seconds is history, not news (a backlog replayed after an
2022
2138
  // upgrade or a long stop): it is passed over, recorded once, never filed
2023
2139
  const maxAge = Number(workflowFor(board.root)?.params?.notice_max_age_seconds) || null;
@@ -2034,7 +2150,7 @@ export function fileBoardMail(board, report, taskId = null) {
2034
2150
  try {
2035
2151
  const task = view(board, db => getTask(db, first.task_id));
2036
2152
  const subject = mail.requester ? `State requests · ${mail.members.length} request(s) · ${task?.title || first.task_id}` : `${task?.title || first.task_id} · ${mail.message_id ? 'Question' : 'Update'}`;
2037
- const filed = fileMail(board, mail.to, mail.text, mail.message_id ?? `board-message-${first.id}`, { ask: Boolean(mail.message_id), subject });
2153
+ const filed = await fileMail(board, mail.to, mail.text, mail.message_id ?? `board-message-${first.id}`, { ask: Boolean(mail.message_id), subject });
2038
2154
  tx(board, db => {
2039
2155
  for (const member of mail.members) if (!db.prepare("SELECT 1 FROM task_events WHERE kind='board_message_sent' AND task_id=? AND json_extract(payload,'$.event')=?").get(member.task_id, member.id)) event(db, member.task_id, 'board_message_sent', { event: member.id, to: mail.to, message_id: filed.message_id, ...(mail.message_id ? { request_id: member.message_id ?? mail.message_id } : {}) });
2040
2156
  });
@@ -2114,17 +2230,18 @@ function sessionRecord(db) {
2114
2230
  * that resumes the session subscribes it again at its launch and each round while it is the card's current run.
2115
2231
  * A creator's or manager's subscription is never ended here: its notices wait for the session to come back.
2116
2232
  */
2117
- function endWorkerSubscriptions(board, fleet, report) {
2233
+ async function endWorkerSubscriptions(board, fleet, report) {
2118
2234
  // each worker subscriber's liveness is read before the write
2119
2235
  const aliveNow = new Map();
2120
- if (fleet.ok) for (const { chat_id: address } of board.db.prepare("SELECT DISTINCT chat_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all()) aliveNow.set(address, subscriberAlive(fleet, address));
2236
+ if (fleet.ok) for (const { chat_id: address } of board.db.prepare("SELECT DISTINCT chat_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all()) aliveNow.set(address, await subscriberAlive(fleet, address));
2121
2237
  tx(board, (db) => {
2122
2238
  const { held, latest } = sessionRecord(db);
2123
2239
  const remove = db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?");
2124
2240
  const ended = new Map();
2125
2241
  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()) {
2126
2242
  const address = row.chat_id, record = latest.get(address);
2127
- if (!record || record.session.released || held.has(address) || (fleet.ok && aliveNow.get(address) === true)) continue;
2243
+ // a session alive, or whose liveness could not be read (unknown, never taken as ended), keeps its subscription
2244
+ if (!record || record.session.released || held.has(address) || (fleet.ok && aliveNow.get(address) !== false)) continue;
2128
2245
  const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at } : null;
2129
2246
  if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
2130
2247
  if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
@@ -2143,8 +2260,8 @@ function endWorkerSubscriptions(board, fleet, report) {
2143
2260
  * another. Each subscription's notices go in order, and its cursor moves past a notice once it is filed (or passed
2144
2261
  * over); a filing that failed here leaves the cursor before it, and the next pass files it.
2145
2262
  */
2146
- function notify(board, fleet, report, entry) {
2147
- endWorkerSubscriptions(board, fleet, report);
2263
+ async function notify(board, fleet, report, entry) {
2264
+ await endWorkerSubscriptions(board, fleet, report);
2148
2265
  for (const sub of subscriptions(board.db).filter((s) => s.platform === 'supercode')) {
2149
2266
  if (stopping) break;
2150
2267
  if (sub.delivery_metadata?.dependent_card && sub.delivery_metadata.waiting_recipient) continue;
@@ -2152,7 +2269,12 @@ function notify(board, fleet, report, entry) {
2152
2269
  // a session the fleet does not list this pass is told next pass: one reading is not a loss (a loaded box drops
2153
2270
  // sessions from a read, and a dropped subscription never came back), and a session that is replaced on its card
2154
2271
  // has its subscription moved when the new one starts
2155
- if (role !== 'creator' && fleet.ok && !subscriberAlive(fleet, sub.chat_id)) continue;
2272
+ if (role !== 'creator' && fleet.ok) {
2273
+ const alive = await subscriberAlive(fleet, sub.chat_id);
2274
+ if (alive === false) continue;
2275
+ // unknown is not absent: its notices wait, and the wait is said, until its liveness can be read
2276
+ if (alive === null) { report.undelivered.push([sub.task_id, sub.chat_id, 'its liveness could not be read; its notices wait until it can be']); continue; }
2277
+ }
2156
2278
  const kinds = heardBy(entry, role);
2157
2279
  if (!kinds.length) continue;
2158
2280
  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`)
@@ -2194,7 +2316,7 @@ function notify(board, fleet, report, entry) {
2194
2316
  if (text) {
2195
2317
  const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
2196
2318
  // filed under its fixed key: a second pass filing the same notice files nothing new
2197
- const filed = fileMail(board, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { wake, subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
2319
+ const filed = await fileMail(board, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { wake, subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
2198
2320
  if (filed.new) tx(board, (db) => event(db, sub.task_id, 'notice_filed', { source_event: ev.id, kind: ev.kind, address: sub.chat_id, message_id: filed.message_id, wake }));
2199
2321
  report.notified.push([sub.task_id, ev.kind, sub.chat_id]);
2200
2322
  }
@@ -2360,7 +2482,7 @@ function mailboxOf(address) {
2360
2482
  * read. Any other message is mail it does not take: it is refused to its sender, in its own mailbox, and filed as read
2361
2483
  * only once that refusal is filed.
2362
2484
  */
2363
- function readMailbox(board, address, report) {
2485
+ async function readMailbox(board, address, report) {
2364
2486
  const box = mailboxOf(address);
2365
2487
  if (!box || !existsSync(join(box, 'new'))) return;
2366
2488
  for (const name of readdirSync(join(box, 'new')).filter((n) => n.endsWith('.json')).sort()) {
@@ -2370,7 +2492,7 @@ function readMailbox(board, address, report) {
2370
2492
  const notice = envelope.kind === 'notice' || envelope.reply_via?.mode === 'none';
2371
2493
  if (!envelope.in_reply_to && !notice) {
2372
2494
  try {
2373
- fileMail(board, envelope.from, `${address} takes answers to its questions and notices only; this message was not taken.`, `board-refused-${envelope.id}`);
2495
+ await fileMail(board, envelope.from, `${address} takes answers to its questions and notices only; this message was not taken.`, `board-refused-${envelope.id}`);
2374
2496
  } catch (error) { report.undelivered.push(['mail', envelope.from, String(error.message).slice(-200)]); continue; }
2375
2497
  report.mailed.push([envelope.from, 'refused']);
2376
2498
  }
@@ -2402,10 +2524,10 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
2402
2524
  fleetRoot = root;
2403
2525
  const loaded = await loadWorkflow(root);
2404
2526
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
2405
- const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
2527
+ const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? await observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
2406
2528
  const out = [];
2407
2529
  // the home's caps count every board, whichever boards are planned
2408
- const held = homeHoldings(root, listBoards(root), lanes, fleet, entry);
2530
+ const held = await homeHoldings(root, listBoards(root), lanes, fleet, entry);
2409
2531
  const caps = capacity(entry.workflow.dispatch?.capacity, held, evaluate);
2410
2532
  // what each cap counts now, before anything this plan would start
2411
2533
  Object.defineProperty(out, 'capacity', { value: caps.view().map((cap) => ({ ...cap, groups: cap.groups.map((g) => ({ ...g, max: capMax(cap, g, entry, lanes) })) })) });
@@ -2415,7 +2537,7 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
2415
2537
  try {
2416
2538
  board.label = boardLabel(root, slug);
2417
2539
  await activatePullRequestReviews(board, entry, report, [], { dry: true, fleet });
2418
- startCards(board, fleet, lanes, settings, report, entry, [], { dry: true, held });
2540
+ await startCards(board, fleet, lanes, settings, report, entry, [], { dry: true, held });
2419
2541
  } finally { board.close(); }
2420
2542
  out.push(report);
2421
2543
  }
@@ -2438,12 +2560,12 @@ function noCodeReviewDue(board, id) {
2438
2560
  /** The arc's session still in a turn: its review, and the close of that session the request brings (end_run), wait for
2439
2561
  * the turn to end, so work in flight is never cut (t_f34710bc's coder had a proof take running when its review was
2440
2562
  * requested at 15:01:13Z on 2026-10-04, and the close ended it). A session the reading cannot place waits on nothing. */
2441
- function turnInFlight(board, id, fleet) {
2563
+ async function turnInFlight(board, id, fleet) {
2442
2564
  if (!fleet?.ok) return null;
2443
2565
  const session = currentRun(board.db, id)?.metadata?.supercode;
2444
2566
  if (!session?.session_id || session.surface === 'headless' || session.close_requested) return null;
2445
2567
  let turn = fleet.sessions?.find((x) => x.address === addressOf(session))?.turn ?? null;
2446
- if (turn == null && session.surface === 'native') try { turn = nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity?.turn ?? null; } catch { /* unread: waits on nothing */ }
2568
+ if (turn == null && session.surface === 'native') try { turn = nativeReading(await readNative(session.machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity?.turn ?? null; } catch { /* unread: waits on nothing */ }
2447
2569
  // a turn stopped on a permission or a question (needs_input) is a turn not finished, as one still working is
2448
2570
  return ['working', 'needs_input'].includes(turn) ? `its session ${addressOf(session)} is in a turn (${turn}); the review is requested once the turn ends` : null;
2449
2571
  }
@@ -2466,26 +2588,26 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
2466
2588
  const ready = view(board, eligible);
2467
2589
  if (!ready) continue;
2468
2590
  if (ready !== true) { report.queued.push([id, `review waits: ${ready.length} open task(s) in the card's Tasks: ${ready[0].id} ${ready[0].title}`]); continue; }
2469
- const inTurn = turnInFlight(board, id, fleet);
2591
+ const inTurn = await turnInFlight(board, id, fleet);
2470
2592
  if (inTurn) { report.queued.push([id, `review waits: ${inTurn}`]); continue; }
2471
2593
  let payload = null;
2472
2594
  try {
2473
- payload = openReviewPullRequests(board, id);
2595
+ payload = await openReviewPullRequests(board, id);
2474
2596
  // The arc's open PRs ask for one review only while some head has no verdict: after changes are
2475
2597
  // requested the next run is the implementer's, until a new head is pushed.
2476
2598
  // An arc that closed its tasks having changed no code gets its one review of the act itself (ADR 0003).
2477
- if (!payload && noCodeReviewDue(board, id)) payload = noCodeChange(board, id);
2599
+ if (!payload && noCodeReviewDue(board, id)) payload = await noCodeChange(board, id);
2478
2600
  if (!payload) { report.queued.push([id, 'review waits: no open PR on its branch, a wt/<card> branch or a registered checkout']); continue; }
2479
2601
  if (payload.waits) { report.queued.push([id, `review waits: ${payload.waits}`]); payload = null; continue; }
2480
- if (!payload.no_code && !reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
2602
+ if (!payload.no_code && !await reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
2481
2603
  report.queued.push([id, `review waits: every open PR head (${payload.pr.join(', ')}) has a verdict or predates the last one; push a commit that answers it`]);
2482
2604
  continue;
2483
2605
  }
2484
- const evidence = payload.no_code ? { required: false, ready: true, reason: null, phase: 'source', no_code: payload.no_code } : inspectCompletionEvidence(board, id, entry, payload, 'request_review');
2606
+ const evidence = payload.no_code ? { required: false, ready: true, reason: null, phase: 'source', no_code: payload.no_code } : await inspectCompletionEvidence(board, id, entry, payload, 'request_review');
2485
2607
  if (!evidence.ready) throw new Error(evidence.reason);
2486
2608
  // a dry run names the review a tick would request and writes nothing
2487
2609
  if (dry) { (report.would_review ??= []).push({ id, prs: payload.pr, heads: payload.pr_head, no_code: payload.no_code ?? null }); continue; }
2488
- refreshCardInputs(board, id, { commits: true });
2610
+ await refreshCardInputs(board, id, { commits: true });
2489
2611
  const moved = tx(board, db => {
2490
2612
  const task = getTask(db, id);
2491
2613
  if (eligible(db) !== true) return null;
@@ -2537,9 +2659,10 @@ export function startDelivery(root, { slugs = null, log = () => {} } = {}) {
2537
2659
 
2538
2660
  /**
2539
2661
  * The delivery pass over every board of the home at `root`: what rounds recorded for after them (each due effect, with
2540
- * its bounded attempts), board mail, subscription notices, and the board's agent records joined to Teams. It runs in
2541
- * its own process (`workflow deliver`, started by serve after a round, one at a time), never inside a dispatch round,
2542
- * so no recipient, door or Teams answer holds a round. Answers one report per board.
2662
+ * its bounded attempts), board mail, subscription notices, and the board's agent records joined to Teams. The owner
2663
+ * runs it in its own (the owner's) process after a round (startDelivery, one at a time, never awaited by the round), and an operator
2664
+ * runs it as `workflow deliver`. Every door, filing and Teams answer it waits on is awaited, never a synchronous wait,
2665
+ * so the owner answers verbs while it runs. Answers one report per board.
2543
2666
  */
2544
2667
  /**
2545
2668
  * Whether the board dispatches: board state, kept in the board's own store (`supercode_board_state`, key `dispatching`)
@@ -2562,7 +2685,7 @@ export async function deliver({ root, slugs = null, controller = false }) {
2562
2685
  // already under way when a board is switched off leaves it); `workflow deliver`, an operator's act, delivers as asked
2563
2686
  const boards = (slugs ?? listBoards(root)).filter((slug) => !controller || dispatchingOf(root, slug).on);
2564
2687
  if (!boards.length) return [];
2565
- const fleet = observeFleet();
2688
+ const fleet = await observeFleet();
2566
2689
  const out = [];
2567
2690
  for (const slug of boards) {
2568
2691
  if (controller && !dispatchingOf(root, slug).on) continue;
@@ -2578,10 +2701,10 @@ export async function deliver({ root, slugs = null, controller = false }) {
2578
2701
  // a controller's pass reads the board's switch before each phase and each effect: switched off meanwhile, what had
2579
2702
  // not begun is left (an operator's `workflow deliver` delivers as asked)
2580
2703
  const on = () => !controller || dispatchingOf(root, slug).on;
2581
- if (on()) { fileBoardMail(board, report); phase('mail'); }
2704
+ if (on()) { await fileBoardMail(board, report); phase('mail'); }
2582
2705
  if (on()) { report.effects = await runEffects(board, [], { pending: true, eligible: controller ? on : null }); phase('effects'); }
2583
- if (on()) { notify(board, fleet, report, entry); phase('notify'); }
2584
- if (on()) { synchronizeAgents(board, report); phase('agents'); }
2706
+ if (on()) { await notify(board, fleet, report, entry); phase('notify'); }
2707
+ if (on()) { await synchronizeAgents(board, report); phase('agents'); }
2585
2708
  report.effects_failed = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NOT NULL').get().n);
2586
2709
  report.effects_waiting = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NULL').get().n);
2587
2710
  } catch (error) { report.error = error.message; }
@@ -2613,17 +2736,18 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2613
2736
  fleetRoot = root;
2614
2737
  discoveries.clear();
2615
2738
  // a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
2616
- const deployment = deploymentReceipt ? workflowRevision(root) : null;
2739
+ const deployment = deploymentReceipt ? await workflowRevision(root) : null;
2617
2740
  const loaded = await loadWorkflow(root);
2618
2741
  const entry = { ...loaded, params: { ...loaded.params, ...params } };
2619
2742
  const needFleet = Object.values(lanes).some((l) => l.surface === 'pane');
2620
2743
  const fleetAt = Date.now();
2621
- const fleet = needFleet ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
2744
+ const fleet = needFleet ? await observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
2622
2745
  const fleetMs = Date.now() - fleetAt;
2746
+ await readMemoryPressure();
2623
2747
  const boards = slugs ?? listBoards(root);
2624
2748
  // what every board of the home holds, for caps whose scope is the whole home (kanban.max_in_progress counts so): every
2625
2749
  // board, including one whose dispatching is off, whose live workers still hold their places
2626
- const held = dispatch ? homeHoldings(root, listBoards(root), lanes, fleet, entry) : [];
2750
+ const held = dispatch ? await homeHoldings(root, listBoards(root), lanes, fleet, entry) : [];
2627
2751
  const out = [];
2628
2752
  for (const slug of boards) {
2629
2753
  if (stopping) break;
@@ -2646,17 +2770,17 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2646
2770
  let mark = Date.now();
2647
2771
  const phase = (name) => { const t = Date.now(); report.ms[name] = t - mark; mark = t; };
2648
2772
 
2649
- readMailbox(board, boardAddress(board.label), report); phase('mailbox');
2773
+ await readMailbox(board, boardAddress(board.label), report); phase('mailbox');
2650
2774
  await refreshRelationAnswers(board); phase('answers');
2651
2775
  const effects = [];
2652
- completeAdoptions(board, fleet, settings, report, lanes); reconnectContinuedSessions(board, fleet, entry, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); queueEffects(board, effects.splice(0)); closeRequested(board, report, { ttl: settings.ttl, fleet }); phase('supervise');
2776
+ await completeAdoptions(board, fleet, settings, report, lanes); await reconnectContinuedSessions(board, fleet, entry, report); await reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); await supervise(board, fleet, settings, report, entry, effects); queueEffects(board, effects.splice(0)); await closeRequested(board, report, { ttl: settings.ttl, fleet }); phase('supervise');
2653
2777
  report.unsettled = [];
2654
2778
  report.promoted = recomputeReady(board, effects, report.unsettled);
2655
2779
  tx(board,db=>auditEndedRuns(db,board,entry));
2656
2780
  if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only, fleet });
2657
- if (dispatch && !stopping) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
2781
+ if (dispatch && !stopping) await startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
2658
2782
  phase('start');
2659
- if (dispatch && !only && !report.paused) observeIdle(board, fleet, settings, report, entry, effects);
2783
+ if (dispatch && !only && !report.paused) await observeIdle(board, fleet, settings, report, entry, effects);
2660
2784
  // what the moves asked for is recorded with them; the delivery pass (`deliver`) carries it out, outside the round
2661
2785
  queueEffects(board, effects);
2662
2786
  phase('goals');
@@ -2676,7 +2800,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
2676
2800
  out.push(report);
2677
2801
  }
2678
2802
  if (deployment && out.every(report => !report.error)) {
2679
- const receipt = retainWorkflowTick(root, deployment);
2803
+ const receipt = await retainWorkflowTick(root, deployment);
2680
2804
  if (receipt && out[0]) out[0].deployment = receipt;
2681
2805
  }
2682
2806
  return out;