@volter/supercode-orchestrator 0.4.160 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/adapters/chat-sdk.mjs +4 -1
- package/adapters/rh2.mjs +18 -20
- package/agent-home.mjs +35 -0
- package/agent-profiles.mjs +53 -0
- package/agents.mjs +443 -0
- package/bin/agent-door.mjs +53 -0
- package/bin/runtime-door.mjs +23 -0
- package/bin/runtime.mjs +59 -4
- package/board/agent-declarations.mjs +29 -0
- package/board/ci.mjs +6 -1
- package/board/cli.mjs +158 -31
- package/board/dispatch.mjs +299 -121
- package/board/engine.mjs +30 -10
- package/board/event-fleet.mjs +59 -0
- package/board/event-relay.mjs +45 -0
- package/board/event-stream.mjs +104 -0
- package/board/incidents.mjs +57 -0
- package/board/published.mjs +86 -0
- package/board/runtime-door.mjs +12 -0
- package/board/store.mjs +39 -6
- package/board/tools.mjs +1 -0
- package/board/wake.mjs +17 -56
- package/board/workflow.mjs +28 -7
- package/board/workspace.mjs +18 -15
- package/commands.mjs +2 -2
- package/conversations.mjs +108 -0
- package/cron.mjs +30 -4
- package/gates.mjs +3 -2
- package/hermes-cli.mjs +19 -2
- package/index.d.ts +43 -2
- package/index.mjs +4 -0
- package/installed-apps.mjs +52 -0
- package/ir.mjs +3 -3
- package/kanban-worker.mjs +16 -6
- package/loop.mjs +211 -21
- package/mcp/bridge.mjs +13 -2
- package/mcp/tools.mjs +17 -0
- package/mcp/verbs.mjs +29 -7
- package/ontology.d.ts +246 -2
- package/operator.mjs +6 -1
- package/package.json +2 -2
- package/reducer.mjs +133 -28
- package/webhook.mjs +8 -4
package/board/dispatch.mjs
CHANGED
|
@@ -15,7 +15,14 @@
|
|
|
15
15
|
// Every side effect follows a committed transition: a card is claimed before its session is launched, and a
|
|
16
16
|
// subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
|
|
17
17
|
// launch and a crash between the two steps is answered by the next tick.
|
|
18
|
+
import { monitorsRuns, failedObservation, surfaceIncident, auditEndedRuns } from './incidents.mjs';
|
|
19
|
+
import { readEventFleet } from './event-fleet.mjs';
|
|
20
|
+
let eventFleetRoot = null;
|
|
18
21
|
import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
22
|
+
import { runtimeDoor } from './runtime-door.mjs';
|
|
23
|
+
import { agentDoor, agentScope, teamsUnavailable, ensureCardAgent, localCardAgent, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds } from '../agents.mjs';
|
|
24
|
+
import { materializeAgentHome } from '../agent-home.mjs';
|
|
25
|
+
import { memorySnapshotSync } from '../hermes-cli.mjs';
|
|
19
26
|
import { createRequire } from 'node:module';
|
|
20
27
|
import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
21
28
|
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -24,7 +31,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
24
31
|
import { createHash, randomUUID } from 'node:crypto';
|
|
25
32
|
import { fileURLToPath } from 'node:url';
|
|
26
33
|
import { view, boardCopyOf, boardPath, claimer, copyRefusal, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
27
|
-
import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, storedStatus, templateValue, workflowFor } from './workflow.mjs';
|
|
34
|
+
import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, projectStatus, storedStatus, templateValue, workflowFor } from './workflow.mjs';
|
|
28
35
|
import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
|
|
29
36
|
import { capacity, harnessDefaults, holdings } from './capacity.mjs';
|
|
30
37
|
import { evaluate, holds, render } from './expr.mjs';
|
|
@@ -46,34 +53,26 @@ export function normalMachine(name) {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/** This machine's name as supercode's addresses spell it: its Teams enrollment, else its host name. */
|
|
56
|
+
let localMachineName;
|
|
49
57
|
export function localMachine() {
|
|
50
|
-
|
|
58
|
+
if (localMachineName) return localMachineName;
|
|
51
59
|
try {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch { /* not enrolled: the host name */ }
|
|
57
|
-
return normalMachine(hostname());
|
|
60
|
+
const reply=runtimeDoor(null,'harness.v1.machine.describe',{}, {timeout:15_000});
|
|
61
|
+
if(reply.code===0){const machine=JSON.parse(reply.stdout);return localMachineName=normalMachine(machine.name??machine.hostname);}
|
|
62
|
+
} catch { /* no local daemon: a plain host still has its OS machine name */ }
|
|
63
|
+
return localMachineName=normalMachine(hostname());
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** The supercode command the dispatcher drives. */
|
|
61
67
|
export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
// An explicit operator override remains authoritative for source development.
|
|
65
|
-
let teamsEntry;
|
|
69
|
+
/** Public CLI calls carry caller attribution; installed clients own transport selection. */
|
|
66
70
|
export function commandEnv(as = null) {
|
|
67
|
-
|
|
68
|
-
try { teamsEntry = createRequire(import.meta.url).resolve('@volter/supercode-teams/bin'); }
|
|
69
|
-
catch { teamsEntry = null; }
|
|
70
|
-
}
|
|
71
|
-
return { ...process.env, ...(!process.env.SUPERCODE_TEAMS_ENTRY && teamsEntry ? { SUPERCODE_TEAMS_ENTRY: teamsEntry } : {}),
|
|
72
|
-
...(as ? { SUPERCODE_CALLER: as } : {}) };
|
|
71
|
+
return { ...process.env, ...(as ? { SUPERCODE_CALLER: as } : {}) };
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
|
|
76
|
-
//
|
|
75
|
+
// `as`: the board this
|
|
77
76
|
// command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
|
|
78
77
|
const env = commandEnv(as);
|
|
79
78
|
const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
|
|
@@ -116,19 +115,11 @@ export function callerSession() {
|
|
|
116
115
|
return claude;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
export function observeFleet() {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
if (!Array.isArray(sessions)) return { ok: false, alive: new Set(), online: new Set(), error: `message list: ${(list.stderr || list.stdout).trim().slice(-300)}` };
|
|
123
|
-
const machines = sc(['teams', 'machines', 'events', '--json']);
|
|
124
|
-
const presence = machines.code === 0 ? lastJson(machines.stdout) : null;
|
|
125
|
-
const online = new Set([localMachine()]);
|
|
126
|
-
for (const m of presence?.items ?? []) if (m.linked) online.add(normalMachine(m.name));
|
|
127
|
-
presenceReading = { known: presence?.connected === true, online };
|
|
118
|
+
export function observeFleet(root = eventFleetRoot) {
|
|
119
|
+
const fleet = readEventFleet(root);
|
|
120
|
+
presenceReading = { known: fleet.presenceKnown === true, online: new Set([localMachine(), ...fleet.online]) };
|
|
128
121
|
notePresence(presenceReading);
|
|
129
|
-
return
|
|
130
|
-
presenceKnown: presence?.connected === true, machineEvents: presence,
|
|
131
|
-
error: presence?.connected ? null : 'machine event stream unavailable; retaining existing runs until availability is known' };
|
|
122
|
+
return fleet;
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
// how the launcher opens each harness a pane lane can run, and how it names the session: Claude Code is told the id
|
|
@@ -142,7 +133,23 @@ const HARNESSES = {
|
|
|
142
133
|
const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
|
|
143
134
|
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
144
135
|
|
|
145
|
-
|
|
136
|
+
// A run Teams never published (one outside a synced root, a scratch workspace's) is read on its own machine: a
|
|
137
|
+
// headless worker by its pid, a pane by that machine's pane list. Null only when the machine cannot answer.
|
|
138
|
+
const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ? fleet.native.get(addressOf(session)).alive : unpublishedAlive(session);
|
|
139
|
+
const paneLists = new Map();
|
|
140
|
+
function unpublishedAlive(session) {
|
|
141
|
+
if (!session) return null;
|
|
142
|
+
if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
|
|
143
|
+
if (!session.pane || !session.machine) return null;
|
|
144
|
+
let entry = paneLists.get(session.machine);
|
|
145
|
+
if (!entry || Date.now() - entry.at > 5000) {
|
|
146
|
+
const r = doorOn(session.machine, 'harness.v1.panes.list', {}, { timeout: 60_000 });
|
|
147
|
+
const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
148
|
+
entry = { at: Date.now(), panes: Array.isArray(panes) ? panes : null };
|
|
149
|
+
paneLists.set(session.machine, entry);
|
|
150
|
+
}
|
|
151
|
+
return entry.panes ? entry.panes.some((p) => p.pane === session.pane && !p.dead) : null;
|
|
152
|
+
}
|
|
146
153
|
|
|
147
154
|
/** Native activity comes from the machine's harness discovery, not catalogue age. */
|
|
148
155
|
function nativeSessions(machine, harness) {
|
|
@@ -224,7 +231,6 @@ export function lanesOf(profiles = {}) {
|
|
|
224
231
|
return out;
|
|
225
232
|
}
|
|
226
233
|
|
|
227
|
-
const onMachine = machine => normalMachine(machine ?? localMachine()) === localMachine() ? [] : ['--machine', machine];
|
|
228
234
|
// The latest presence reading (observeFleet). A machine it reads as offline is not asked: each remote call would
|
|
229
235
|
// wait out its timeout, and one offline machine held every close and launch on this one.
|
|
230
236
|
let presenceReading = null;
|
|
@@ -232,45 +238,24 @@ const offlineMachine = (machine) => {
|
|
|
232
238
|
const m = normalMachine(machine ?? localMachine());
|
|
233
239
|
return m !== localMachine() && presenceReading?.known === true && !presenceReading.online.has(m);
|
|
234
240
|
};
|
|
235
|
-
/**
|
|
236
|
-
const
|
|
241
|
+
/** A machine's public door (`harness.v1.*`), answered at once when the machine reads as offline. */
|
|
242
|
+
const doorOn = (machine, method, params, options) => offlineMachine(machine)
|
|
237
243
|
? { code: 1, stdout: '', stderr: `machine ${normalMachine(machine)} is offline` }
|
|
238
|
-
:
|
|
244
|
+
: runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
|
|
239
245
|
const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
|
|
240
246
|
|
|
241
247
|
// ------------------------------------------------------------------ supervise
|
|
242
248
|
|
|
243
249
|
function supervise(board, fleet, settings, report, entry, effects) {
|
|
244
250
|
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();
|
|
245
|
-
//
|
|
246
|
-
|
|
251
|
+
// Published native observations are the only liveness input. Missing data remains unknown. A launch the
|
|
252
|
+
// observations show stopped is asked, through its machine's door, how it ended.
|
|
247
253
|
const exits = new Map();
|
|
248
|
-
const native = new Map(); fleet.native = new Map();
|
|
249
254
|
for (const { id } of rows) {
|
|
250
255
|
const session = currentRun(board.db, id)?.metadata?.supercode;
|
|
251
|
-
if (!session?.
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
try { native.set(key, nativeSessions(session.machine, session.harness)); }
|
|
255
|
-
catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
|
|
256
|
-
}
|
|
257
|
-
fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
|
|
258
|
-
}
|
|
259
|
-
for (const { id } of rows) {
|
|
260
|
-
const s = currentRun(board.db, id)?.metadata?.supercode;
|
|
261
|
-
if (!s?.pane || observed.has(s.machine)) continue;
|
|
262
|
-
const r = scOn(s.machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
|
|
263
|
-
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
264
|
-
observed.set(s.machine, Array.isArray(list) ? new Set(list.filter(p => !p.dead).map((p) => p.pane)) : null);
|
|
265
|
-
if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
|
|
266
|
-
}
|
|
267
|
-
for (const {id} of rows) {
|
|
268
|
-
const session=currentRun(board.db,id)?.metadata?.supercode;
|
|
269
|
-
const panes=observed.get(session?.machine);
|
|
270
|
-
if(session?.launch_key && panes instanceof Set && !panes.has(session.pane)) {
|
|
271
|
-
try { const receipt=launchReceipt(board,session);if(receipt?.live===false&&receipt.exit)exits.set(id,receipt.exit); }
|
|
272
|
-
catch(error){report.failed.push([id,`exit receipt unavailable; retaining liveness observation: ${error.message}`]);}
|
|
273
|
-
}
|
|
256
|
+
if (!session?.launch_key || !fleet.ok || sessionAlive(fleet, session) !== false) continue;
|
|
257
|
+
try { const receipt = launchReceipt(board, session); if (receipt?.live === false && receipt.exit) exits.set(id, receipt.exit); }
|
|
258
|
+
catch (error) { report.failed.push([id, `exit receipt unavailable; retaining liveness observation: ${error.message}`]); }
|
|
274
259
|
}
|
|
275
260
|
for (const { id } of rows) {
|
|
276
261
|
tx(board, (db) => {
|
|
@@ -278,20 +263,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
278
263
|
if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
|
|
279
264
|
const runRow = currentRun(db, id);
|
|
280
265
|
let session = runRow?.metadata?.supercode ?? null;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (found && found !== session.pane) {
|
|
286
|
-
session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
|
|
287
|
-
setRunSession(db, runRow.id, session);
|
|
288
|
-
event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
|
|
289
|
-
}
|
|
266
|
+
const liveBinding=fleet.sessions?.find(item=>item.address===addressOf(session)&&item.state==='running'&&item.pane);
|
|
267
|
+
if(liveBinding?.pane&&liveBinding.pane!==session.pane){
|
|
268
|
+
session={...session,pane:liveBinding.pane,missing_since:null};setRunSession(db,runRow.id,session);
|
|
269
|
+
event(db,id,'session_rebound',{session_id:session.session_id,pane:session.pane},runRow.id);
|
|
290
270
|
}
|
|
291
271
|
const t = now();
|
|
292
272
|
// what the dispatcher saw becomes a platform event; the card's workflow says what it does
|
|
293
273
|
const observe = (name, payload, bucket) => {
|
|
294
274
|
try {
|
|
275
|
+
if (monitorsRuns(entry) && failedObservation(name)) {
|
|
276
|
+
surfaceIncident(db, board, entry, id, name, payload);
|
|
277
|
+
if (bucket) report[bucket].push(id);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
295
280
|
const r = fire(db, board, entry, id, name, { roles: ['platform'], payload });
|
|
296
281
|
effects.push(...r.effects);
|
|
297
282
|
if (bucket) report[bucket].push(id);
|
|
@@ -319,8 +304,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
319
304
|
// Availability is the same retained linked/unlinked feed subscribers hear.
|
|
320
305
|
// A failed stream is unknown, not evidence that every remote machine left.
|
|
321
306
|
if (session?.session_id && session.surface !== 'headless') {
|
|
322
|
-
const
|
|
323
|
-
const reachable = panes instanceof Set || fleet.online.has(session.machine);
|
|
307
|
+
const reachable = fleet.online.has(session.machine);
|
|
324
308
|
if (!reachable && !fleet.presenceKnown) return;
|
|
325
309
|
if (!reachable) {
|
|
326
310
|
const next = {...session, offline_since: session.offline_since ?? t};
|
|
@@ -341,8 +325,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
341
325
|
}
|
|
342
326
|
}
|
|
343
327
|
// Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
|
|
344
|
-
const
|
|
345
|
-
const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
|
|
328
|
+
const paneLive = session?.session_id ? sessionAlive(fleet,session) : null;
|
|
346
329
|
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
|
|
347
330
|
// (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
|
|
348
331
|
if ((!task.claim_lock || !task.claim_expires) && !live) {
|
|
@@ -378,6 +361,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
378
361
|
// follows, and it is not a crash to restart
|
|
379
362
|
const ended = { code: exit.failed ? 1 : 0, error: `native session ${session.session_id} ended: ${exit.how}`, session_id: session.session_id, pane: session.pane };
|
|
380
363
|
if (!exit.failed) {
|
|
364
|
+
if (monitorsRuns(entry)) { observe('session_closed', {...ended,exit},'crashed'); return; }
|
|
381
365
|
try {
|
|
382
366
|
const r = fire(db, board, entry, id, 'session_closed', { roles: ['platform'], payload: ended });
|
|
383
367
|
effects.push(...r.effects); (report.closed_by_hand ??= []).push(id);
|
|
@@ -387,11 +371,10 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
387
371
|
observe('exited', ended, 'crashed');
|
|
388
372
|
return;
|
|
389
373
|
}
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const machineUp = paneReading instanceof Set || fleet.online.has(session.machine);
|
|
374
|
+
if (!fleet.ok) return;
|
|
375
|
+
const alive = sessionAlive(fleet,session);
|
|
376
|
+
if (alive == null) return;
|
|
377
|
+
const machineUp = fleet.online.has(session.machine);
|
|
395
378
|
const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
|
|
396
379
|
const missing = !alive && machineUp && !young;
|
|
397
380
|
const next = { ...session };
|
|
@@ -450,7 +433,7 @@ function stopSession(board, session) {
|
|
|
450
433
|
return;
|
|
451
434
|
}
|
|
452
435
|
if (session.pane) {
|
|
453
|
-
const r =
|
|
436
|
+
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)});
|
|
454
437
|
if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
|
|
455
438
|
}
|
|
456
439
|
}
|
|
@@ -537,6 +520,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
537
520
|
const stop = paused(board.root);
|
|
538
521
|
if (stop) { report.paused = stop; return; }
|
|
539
522
|
const { workflow, params } = entry;
|
|
523
|
+
if (!dry) tx(board, db => auditEndedRuns(db, board, entry));
|
|
540
524
|
const d = workflow.dispatch;
|
|
541
525
|
const pressure = memoryPressure();
|
|
542
526
|
const tickState = { local_started: 0 };
|
|
@@ -633,7 +617,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
633
617
|
} catch (error) {
|
|
634
618
|
if (error.launchPending) { report.queued.push([task.id, error.message]); continue; }
|
|
635
619
|
const payload = { error: String(error.message).slice(-500), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
|
|
636
|
-
try { const r = tx(board, (db)
|
|
620
|
+
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 */ }
|
|
637
621
|
report.failed.push([task.id, String(error.message).slice(-300)]);
|
|
638
622
|
}
|
|
639
623
|
}
|
|
@@ -656,7 +640,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
656
640
|
const where = whereOf(board, task);
|
|
657
641
|
// a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
|
|
658
642
|
const arcContext = task.subtask_of ? view(board, (db) => cardContext(db, task.subtask_of)) : '';
|
|
659
|
-
|
|
643
|
+
let prompt = render(spec.prompt, { ...scope, where, context: view(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
|
|
660
644
|
// the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
|
|
661
645
|
// is a template, so a step can ask for isolation by naming a new one
|
|
662
646
|
const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
|
|
@@ -668,12 +652,57 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
668
652
|
// the model it runs is recorded with the session, for caps that count by model
|
|
669
653
|
const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
|
|
670
654
|
try {
|
|
655
|
+
const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context),machine=normalMachine(task.machine)===localMachine()?null:task.machine;
|
|
656
|
+
const cardOptions={root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
|
|
657
|
+
profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null,machine,context};
|
|
658
|
+
// Situation 16: with no reachable Teams the card's agent is its locally minted identity, declared later under the same id.
|
|
659
|
+
let agent=null,previous=null,reader=null;
|
|
660
|
+
if(!team.offline){
|
|
661
|
+
try{
|
|
662
|
+
agent=ensureCardAgent(cardOptions);
|
|
663
|
+
previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
|
|
664
|
+
reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
|
|
665
|
+
}catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
|
|
666
|
+
}
|
|
667
|
+
agent??=localCardAgent(cardOptions);
|
|
668
|
+
// G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
|
|
669
|
+
const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
|
|
670
|
+
const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
|
|
671
|
+
if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
|
|
672
|
+
previous??=localAgentSessions(board.root,team.org,agent.identity.id);
|
|
673
|
+
prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
|
|
674
|
+
Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
|
|
675
|
+
agent_profile_intent:structuredClone(agent.intended.resolved),
|
|
676
|
+
agent_env:agentEnvironment(agent,{root:board.root,context:reader?.context})});
|
|
677
|
+
// The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
|
|
678
|
+
// another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
|
|
679
|
+
const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
|
|
680
|
+
if(surface==='headless')meta.agent_env.HERMES_HOME=agentHome;
|
|
681
|
+
else{const memory=memorySnapshotSync(agentHome);if(memory)prompt=`${prompt}\n\n${memory}`;}
|
|
671
682
|
const result = surface === 'pane'
|
|
672
683
|
? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
|
|
673
684
|
: launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
|
|
674
|
-
tx(board,db=>
|
|
685
|
+
tx(board,db=>{
|
|
686
|
+
setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
|
|
687
|
+
event(db,id,'restart_authorization_consumed',{},runId);
|
|
688
|
+
});
|
|
689
|
+
if(result?.session_id){
|
|
690
|
+
const ref=`${result.harness}:${result.session_id}`;
|
|
691
|
+
recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
|
|
692
|
+
if(agent.synced!==false){
|
|
693
|
+
try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
|
|
694
|
+
catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
675
697
|
return result;
|
|
676
698
|
} catch(error) {
|
|
699
|
+
// An issued asynchronous launch may bind later. Its original permission
|
|
700
|
+
// still belongs to this attempt, never a future lost-session restart. A
|
|
701
|
+
// deferred launch was never issued: its run keeps the permission for its retry.
|
|
702
|
+
if(!error.launchDeferred)tx(board,db=>{
|
|
703
|
+
setRunSession(db,runId,{restart_authorized:false});
|
|
704
|
+
event(db,id,'restart_authorization_consumed',{},runId);
|
|
705
|
+
});
|
|
677
706
|
if(error.launchDeferred) {
|
|
678
707
|
tx(board,db=>{
|
|
679
708
|
const previous=currentRun(db,id)?.metadata?.supercode;
|
|
@@ -688,22 +717,33 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
688
717
|
|
|
689
718
|
/** Start one card now by a person's word (a triage card's session, a manager's start): the workflow's start event. */
|
|
690
719
|
export async function startCard(board, id, { roles = ['manager'], lanes, settings } = {}) {
|
|
720
|
+
// the board's own published fleet projection, as a tick reads it: a manager's or creator's start that continues the
|
|
721
|
+
// slot's session checks that session through it
|
|
722
|
+
eventFleetRoot = board.root;
|
|
691
723
|
const entry = await loadWorkflow(board.root);
|
|
692
724
|
const fleet = observeFleet();
|
|
693
725
|
const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
|
|
694
726
|
const moved = tx(board, (db) => {
|
|
695
727
|
assertWorkspaceOwner(db, before, checked);
|
|
696
728
|
if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
|
|
697
|
-
if (!roles.
|
|
729
|
+
if (!roles.some(role => ['manager', 'creator'].includes(role))) throw new Error('Only the manager or creator starts a held card; request its decision.');
|
|
698
730
|
event(db, id, 'dispatch_released', { reason: 'explicit start' });
|
|
699
|
-
|
|
731
|
+
requestSessionClose(db,id);
|
|
732
|
+
const moved = fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } });
|
|
733
|
+
setRunSession(db, getTask(db,id).current_run_id, { restart_authorized: true });
|
|
734
|
+
return moved;
|
|
700
735
|
});
|
|
701
736
|
const task = getTaskNow(board, id);
|
|
702
737
|
const spec = runSpec(entry, moved.to, task);
|
|
703
738
|
const profile = spec ? view(board, (db) => evaluate(entry.workflow.roles?.[spec.role] ?? 'card.assignee', scopeOf(db, board, task, { params: entry.params }))) : null;
|
|
704
739
|
const lane = profile ? lanes[profile] : null;
|
|
705
740
|
if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
|
|
706
|
-
|
|
741
|
+
if (!lane) return null;
|
|
742
|
+
try { return launchRun(board,entry,id,lane,fleet,settings); }
|
|
743
|
+
catch (error) {
|
|
744
|
+
if (monitorsRuns(entry) && !error.launchPending) tx(board,db=>surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message}));
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
707
747
|
}
|
|
708
748
|
|
|
709
749
|
/** Reconcile durable close intent and ended runs, including old unverified closed_at records.
|
|
@@ -740,7 +780,9 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
740
780
|
(report.closed ??= []).push({ task_id: row.task_id, ...session, receipt });
|
|
741
781
|
continue;
|
|
742
782
|
}
|
|
743
|
-
|
|
783
|
+
// A receipt that names its pane and reads it not live is terminal enough to settle, with or without a status (a
|
|
784
|
+
// daemon that predates launch statuses answers none): the board's own pane reading below confirms the close
|
|
785
|
+
if (receipt?.pane && receipt.status !== 'issued' && (receipt.status || typeof receipt.live === 'boolean' || receipt.exit)) { bindLaunch(board, row, session, receipt.pane, session.session_id); session = { ...session, pane: receipt.pane }; }
|
|
744
786
|
else {
|
|
745
787
|
const error = `close deferred: ${receiptError ?? `awaiting terminal launch receipt ${session.launch_key} (${receipt?.status ?? 'unknown'})`}`;
|
|
746
788
|
tx(board, (db) => {
|
|
@@ -751,6 +793,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
751
793
|
continue;
|
|
752
794
|
}
|
|
753
795
|
}
|
|
796
|
+
if (session?.monitor_held && !session.close_requested) continue; // only an explicit close action ends this process
|
|
754
797
|
if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
755
798
|
if (caller && session.session_id === caller) continue; // let the command that ended its own run return
|
|
756
799
|
const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)`).all());
|
|
@@ -769,7 +812,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
769
812
|
}
|
|
770
813
|
} else {
|
|
771
814
|
if (!panes.has(machine)) {
|
|
772
|
-
const r =
|
|
815
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
|
|
773
816
|
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
774
817
|
panes.set(machine, Array.isArray(list) ? { live: new Set(list.filter(p => !p.dead).map((p) => p.pane)) } : { error: `cannot read panes on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}` });
|
|
775
818
|
}
|
|
@@ -829,6 +872,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
829
872
|
threshold_seconds: settings.idleQuiet, turns_before: Number(session.goal_turns ?? 0),
|
|
830
873
|
};
|
|
831
874
|
const retain = (decision, extra = {}) => {
|
|
875
|
+
// A reading that decides nothing is kept once per state: rewriting it each pass would wake the next pass.
|
|
876
|
+
const prior = session.idle_observation;
|
|
877
|
+
if (!['dispatched', 'dispatch_failed'].includes(decision) && prior?.decision === decision && prior.last_message_at_ms === reading.last_message_at_ms && prior.status === reading.status) return prior;
|
|
832
878
|
const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
|
|
833
879
|
const patch = { idle_observation: observation };
|
|
834
880
|
if (decision === 'below_threshold') patch.idle_below_threshold = observation;
|
|
@@ -996,9 +1042,9 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
|
|
|
996
1042
|
|
|
997
1043
|
/** A transport error is not a negative launch receipt. The key remains owned by its run. */
|
|
998
1044
|
function launchReceipt(board, session) {
|
|
999
|
-
const r =
|
|
1045
|
+
const r = doorOn(session.machine, 'harness.v1.launches.get',{key:session.launch_key},
|
|
1000
1046
|
{ timeout: 30_000, as: boardAddress(board.label) });
|
|
1001
|
-
if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: install the matching
|
|
1047
|
+
if (r.code !== 0) throw unavailable(`launch receipt unavailable on ${session.machine}: install the matching machine capability (harness.v1.launches.get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1002
1048
|
if (r.stdout.trim() === 'null') return null;
|
|
1003
1049
|
const receipt = lastJson(r.stdout);
|
|
1004
1050
|
if (!receipt || receipt.key !== session.launch_key) throw unavailable(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
|
|
@@ -1006,7 +1052,7 @@ function launchReceipt(board, session) {
|
|
|
1006
1052
|
}
|
|
1007
1053
|
|
|
1008
1054
|
function cancelUnissuedLaunch(board, session) {
|
|
1009
|
-
const result =
|
|
1055
|
+
const result = doorOn(session.machine, 'harness.v1.launches.cancel', {key:session.launch_key},
|
|
1010
1056
|
{ timeout: 30_000, as: boardAddress(board.label) });
|
|
1011
1057
|
if (result.code !== 0) throw unavailable(`launch cancellation unavailable on ${session.machine}; retain ${session.launch_key}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
|
|
1012
1058
|
const receipt = lastJson(result.stdout);
|
|
@@ -1034,7 +1080,8 @@ function retireUnissuedLaunch(board, row, session, receipt, { entry = null, effe
|
|
|
1034
1080
|
forgotten: saved.mode === 'fresh' ? true : Boolean(saved.forgotten) });
|
|
1035
1081
|
event(db, row.task_id, 'session_launch_cancelled', { key: session.launch_key, machine: session.machine, receipt }, row.id);
|
|
1036
1082
|
if (!current.ended_at) {
|
|
1037
|
-
const
|
|
1083
|
+
const payload = { error: `launch ${session.launch_key} was not issued; its machine retired the key` };
|
|
1084
|
+
const moved = monitorsRuns(entry) ? (surfaceIncident(db,board,entry,row.task_id,'spawn_failed',payload), {effects:[]}) : fire(db,board,entry,row.task_id,'spawn_failed',{roles:['platform'],payload});
|
|
1038
1085
|
collected.push(...moved.effects);
|
|
1039
1086
|
}
|
|
1040
1087
|
event(db, row.task_id, 'session_closed', { key: session.launch_key, machine: session.machine, reason: 'launch not issued', receipt }, row.id);
|
|
@@ -1065,6 +1112,7 @@ function bindLaunch(board, row, session, pane, sid) {
|
|
|
1065
1112
|
|
|
1066
1113
|
/** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
|
|
1067
1114
|
export function reconcileCallerLaunch(board, sid) {
|
|
1115
|
+
eventFleetRoot = board.root;
|
|
1068
1116
|
if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
|
|
1069
1117
|
WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
|
|
1070
1118
|
const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
|
|
@@ -1089,7 +1137,63 @@ export function reconcileCallerLaunch(board, sid) {
|
|
|
1089
1137
|
}
|
|
1090
1138
|
|
|
1091
1139
|
function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
|
|
1140
|
+
// A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
|
|
1141
|
+
// A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
|
|
1142
|
+
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())
|
|
1143
|
+
.map(row=>{const session=json(row.metadata,{})?.supercode;return [session?.session_id?addressOf(session):`run:${row.id}`,row];})).values()];
|
|
1144
|
+
const orgs=new Map();
|
|
1145
|
+
for(const row of agentRuns){
|
|
1146
|
+
const session=json(row.metadata,{})?.supercode;
|
|
1147
|
+
if(!session?.session_id||!session.agent_org)continue;
|
|
1148
|
+
const observation=fleet.sessions?.find(item=>item.address===addressOf(session))?.identity;
|
|
1149
|
+
if(observation?.agent?.id===session.agent_id&&session.agent_profile_revision!==observation.revision){
|
|
1150
|
+
const mismatch=agentProfileMismatch(session.agent_profile_intent,observation.resolved);
|
|
1151
|
+
tx(board,db=>{
|
|
1152
|
+
// Rule 4: a run whose recorded profile differs from its agent's intent is shown and not counted.
|
|
1153
|
+
setRunSession(db,row.id,{agent_profile_recorded:observation.resolved,agent_profile_revision:observation.revision,agent_profile_mismatch:mismatch,counted:!mismatch.differences.length});
|
|
1154
|
+
event(db,row.task_id,mismatch.differences.length?'agent_profile_drift':'agent_profile_recorded',{agent:session.agent_id,session:session.session_id,comparison:mismatch},row.id);
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
recordAgentRun(board.root,session.agent_org,session.agent_id,{session:`${session.harness}:${session.session_id}`,machine:session.machine,run:row.id});
|
|
1158
|
+
orgs.set(session.agent_org,session.agent_context);
|
|
1159
|
+
}
|
|
1160
|
+
// Identities minted while Teams was absent are declared under their ids before their sessions are joined.
|
|
1161
|
+
const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context);
|
|
1162
|
+
if(!scope.offline){
|
|
1163
|
+
orgs.set(scope.org,orgs.get(scope.org)??context);
|
|
1164
|
+
for(const pending of synchronizeLocalAgents({root:board.root,profiles:entry.profiles,context}))report.queued.push([pending.agent,`agent declaration pending: ${pending.error}`]);
|
|
1165
|
+
}
|
|
1166
|
+
orgs.delete('local');
|
|
1167
|
+
// G3: an ended run's session usage, read once from its harness's own records on its machine
|
|
1168
|
+
for(const org of new Set(agentRuns.map(row=>json(row.metadata,{})?.supercode?.agent_org).filter(Boolean))){
|
|
1169
|
+
let read=0;
|
|
1170
|
+
for(const row of runsAwaitingUsage(board.root,org)){
|
|
1171
|
+
const run=agentRuns.find(item=>String(item.id)===String(row.run_id));
|
|
1172
|
+
if(!run?.ended_at||read>=5)continue;
|
|
1173
|
+
const [harness,...rest]=row.session_ref.split(':'),machine=row.machine&&normalMachine(row.machine)!==localMachine()?row.machine:null;
|
|
1174
|
+
// the usage door loads the session by its whole locator, which discovery names
|
|
1175
|
+
const sessionId=rest.join(':'),found=runtimeDoor(machine,'harness.v1.sessions.discover',{query:sessionId,harnesses:[harness],limit:200},{timeout:30_000});
|
|
1176
|
+
const locator=found.code===0?json(found.stdout.trim(),{})?.sessions?.find(item=>item.locator?.harness===harness&&item.locator?.session_id===sessionId)?.locator:null;
|
|
1177
|
+
const reply=locator?runtimeDoor(machine,'harness.v1.sessions.usage',{locator},{timeout:30_000}):{code:1};read++;
|
|
1178
|
+
if(reply.code===0)recordRunUsage(board.root,org,row.session_ref,json(reply.stdout.trim(),{})?.usage??[]);
|
|
1179
|
+
// a session unreadable a day after its run ended is recorded as having no usage record, so it stops retrying
|
|
1180
|
+
else if(now()-Number(run.ended_at)>86400)recordRunUsage(board.root,org,row.session_ref,[]);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
for(const assignment of activeAgentAssignments(board.root)){
|
|
1184
|
+
if(assignment.kind!=='card')continue;
|
|
1185
|
+
const task=getTaskNow(board,assignment.target);
|
|
1186
|
+
if(task&&(task.archived||['done','cancelled'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
1187
|
+
}
|
|
1188
|
+
for(const [org,context] of orgs)for(const pending of synchronizeAgentEnds(board.root,org,{context}))report.queued.push([pending.agent,`agent end synchronization pending: ${pending.error}`]);
|
|
1189
|
+
for(const [org,context] of orgs)for(const row of pendingAgentRuns(board.root,org)){
|
|
1190
|
+
const launch=agentRuns.find(run=>String(run.id)===String(row.run_id));
|
|
1191
|
+
try{synchronizeAgentRun(board.root,org,row,{context,intended:json(launch?.metadata,{})?.supercode?.agent_profile_intent});}
|
|
1192
|
+
catch(error){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);}
|
|
1193
|
+
}
|
|
1092
1194
|
const deferred=view(board,db=>db.prepare("SELECT t.id,r.started_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());
|
|
1195
|
+
// A deferred launch was never issued (a reading or door was not yet available): it is the start its manager, creator
|
|
1196
|
+
// or workflow asked for, carried out within the launch window; supervise surfaces it once when the window ends
|
|
1093
1197
|
for(const {id,started_at} of deferred) {
|
|
1094
1198
|
if (now() - started_at >= settings.launchWindow) continue;
|
|
1095
1199
|
try {
|
|
@@ -1101,7 +1205,7 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
|
|
|
1101
1205
|
if(session)report.started.push([id,session.address??session.session_id,session.mode]);
|
|
1102
1206
|
}catch(error){
|
|
1103
1207
|
if(error.launchPending){report.queued.push([id,error.message]);continue;}
|
|
1104
|
-
const moved=tx(board,db=>fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
|
|
1208
|
+
const moved=tx(board,db=>monitorsRuns(entry)?(surfaceIncident(db,board,entry,id,'spawn_failed',{error:error.message}),{effects:[]}):fire(db,board,entry,id,'spawn_failed',{roles:['platform'],payload:{error:error.message}}));effects.push(...moved.effects);
|
|
1105
1209
|
report.failed.push([id,error.message]);
|
|
1106
1210
|
}
|
|
1107
1211
|
}
|
|
@@ -1130,7 +1234,9 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
|
|
|
1130
1234
|
const moved = tx(board, (db) => {
|
|
1131
1235
|
if (getTask(db, row.task_id)?.current_run_id !== row.id) return null;
|
|
1132
1236
|
setRunSession(db, row.id, { launch_pending: false });
|
|
1133
|
-
|
|
1237
|
+
const payload = { error: `launch ${s.launch_key} ended before session binding` };
|
|
1238
|
+
if (monitorsRuns(entry)) { surfaceIncident(db, board, entry, row.task_id, 'spawn_failed', payload); return { effects: [] }; }
|
|
1239
|
+
return fire(db, board, entry, row.task_id, 'spawn_failed', { roles: ['platform'], payload });
|
|
1134
1240
|
});
|
|
1135
1241
|
if (moved) effects.push(...moved.effects);
|
|
1136
1242
|
}
|
|
@@ -1153,11 +1259,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1153
1259
|
task = { ...task, workspace_path: made };
|
|
1154
1260
|
}
|
|
1155
1261
|
const model = task.model_override ?? lane.model;
|
|
1262
|
+
if(meta.agent_profile_intent)meta.agent_profile_intent.worker={...meta.agent_profile_intent.worker,harness:lane.harness,model:model??null,cwd:task.workspace_path};
|
|
1156
1263
|
const tail = model ? ['--model', model] : [];
|
|
1157
1264
|
if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
|
|
1158
1265
|
const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
|
|
1159
1266
|
const how = HARNESSES[harness];
|
|
1160
|
-
const base = ['open', '--new', how.program, '--detach', `--window=${window}`, '--on', task.machine, '--cwd', task.workspace_path
|
|
1267
|
+
const base = ['open', '--new', how.program, '--detach', `--window=${window}`, '--on', task.machine, '--cwd', task.workspace_path, '--env', 'SUPERCODE_BOARD_PROCESS_GUARD=1',
|
|
1268
|
+
...Object.entries(meta.agent_env??{}).flatMap(([key,value])=>['--env',`${key}=${value}`])];
|
|
1161
1269
|
// the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
|
|
1162
1270
|
// a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
|
|
1163
1271
|
// session starts (never two processes on one session)
|
|
@@ -1171,7 +1279,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1171
1279
|
if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
|
|
1172
1280
|
const fresh = observeFleet();
|
|
1173
1281
|
if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
|
|
1174
|
-
const r =
|
|
1282
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1175
1283
|
const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
1176
1284
|
if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1177
1285
|
const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
|
|
@@ -1186,6 +1294,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1186
1294
|
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'}`);
|
|
1187
1295
|
}
|
|
1188
1296
|
const openOwned = (key, sid, mode, args) => {
|
|
1297
|
+
const description = doorOn(machine,'harness.v1.machine.describe',{});
|
|
1298
|
+
const policy = description.code === 0 ? json(description.stdout,null)?.boardProcessPolicy : null;
|
|
1299
|
+
if (policy?.version !== 1 || !policy.harnesses?.includes(harness)) throw unavailable(`machine ${machine} has no native board process guard for ${harness}; no unguarded session was launched`);
|
|
1189
1300
|
launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
|
|
1190
1301
|
tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
|
|
1191
1302
|
session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
|
|
@@ -1236,7 +1347,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1236
1347
|
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`);
|
|
1237
1348
|
if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
|
|
1238
1349
|
// the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
|
|
1239
|
-
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, ...kept } = prev;
|
|
1350
|
+
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;
|
|
1240
1351
|
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
1241
1352
|
// a kept session is told what this run is for (its card came back to it, with what happened since)
|
|
1242
1353
|
tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
|
|
@@ -1252,6 +1363,15 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1252
1363
|
}
|
|
1253
1364
|
}
|
|
1254
1365
|
} else if (prev.resumable !== false) {
|
|
1366
|
+
const authorization = view(board, db => currentRun(db,task.id)?.metadata?.supercode?.restart_authorized);
|
|
1367
|
+
const released = view(board,db=>{
|
|
1368
|
+
const last = db.prepare("SELECT id,kind FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','restart_authorization_consumed') ORDER BY id DESC LIMIT 1").get(task.id);
|
|
1369
|
+
return last?.kind === 'dispatch_released';
|
|
1370
|
+
});
|
|
1371
|
+
// Only a session whose run ended in a monitored incident waits for its manager or creator; one a workflow
|
|
1372
|
+
// transition closed (a reviewer after its verdict, an implementer whose card came back) is the slot's
|
|
1373
|
+
// deliberate handoff, resumed for the slot's next run like any other
|
|
1374
|
+
if (monitorsRuns(workflowFor(board.root)) && prev.monitor_held && !authorization && !released) throw new Error(`session ${prev.session_id} is stopped by incident ${prev.incident ?? 'unknown'}; only its manager or creator may restart it with workflow start`);
|
|
1255
1375
|
// a resumed session is told what this run is for, as a kept one is: the run's own prompt (a reviewer resumed for
|
|
1256
1376
|
// its next round gets that round's prompt, its arc's context with it), not only that it was resumed
|
|
1257
1377
|
const told = input ? `The board resumed this session for this run of card ${task.id}.\n\n${input}` : `The board resumed this session after it was lost; you are still on card ${task.id}: ${task.title}. Carry on.`;
|
|
@@ -1304,7 +1424,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1304
1424
|
|
|
1305
1425
|
/** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
|
|
1306
1426
|
function panePresent(machine, pane) {
|
|
1307
|
-
const r =
|
|
1427
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1308
1428
|
const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
1309
1429
|
if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1310
1430
|
return panes.some((p) => p.pane === pane && !p.dead);
|
|
@@ -1319,6 +1439,7 @@ function paneSurvives(machine, pane) {
|
|
|
1319
1439
|
/** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
|
|
1320
1440
|
function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
|
|
1321
1441
|
const workspace = resolveLocalWorkspace(board, task);
|
|
1442
|
+
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};
|
|
1322
1443
|
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));
|
|
1323
1444
|
// the card's own skills and the ones its status's run names (a reviewer's, for one)
|
|
1324
1445
|
const skills = [...new Set([...task.skills, ...runSkills])];
|
|
@@ -1337,7 +1458,7 @@ function launchHeadless(board, task, runId, lane, settings, { prompt = null, ski
|
|
|
1337
1458
|
env: {
|
|
1338
1459
|
SUPERCODE_BOARD_EXIT_FILE: exitFile,
|
|
1339
1460
|
...(task.goal_mode ? { HERMES_KANBAN_GOAL_MODE: '1', ...(task.goal_max_turns ? { HERMES_KANBAN_GOAL_MAX_TURNS: String(task.goal_max_turns) } : {}) } : {}),
|
|
1340
|
-
...process.env, HERMES_HOME: lane.dir, HERMES_PROFILE: lane.name, HERMES_KANBAN_TASK: task.id, HERMES_KANBAN_WORKSPACE: workspace,
|
|
1461
|
+
...process.env, ...meta.agent_env, HERMES_HOME: meta.agent_env?.HERMES_HOME??lane.dir, HERMES_PROFILE: lane.name, HERMES_KANBAN_TASK: task.id, HERMES_KANBAN_WORKSPACE: workspace,
|
|
1341
1462
|
HERMES_KANBAN_RUN_ID: String(runId), HERMES_KANBAN_DB: board.path, HERMES_KANBAN_BOARD: board.slug, HERMES_SESSION_SOURCE: 'kanban',
|
|
1342
1463
|
HERMES_KANBAN_CLAIM_LOCK: claimer(), ...(prompt ? { SUPERCODE_BOARD_PROMPT: prompt } : {}), ...(task.tenant ? { HERMES_TENANT: task.tenant } : {}),
|
|
1343
1464
|
...(task.branch_name ? { HERMES_KANBAN_BRANCH: task.branch_name } : {}),
|
|
@@ -1396,14 +1517,14 @@ export function requestAdoption(board, id, who, { workspace = null, assignee = n
|
|
|
1396
1517
|
}
|
|
1397
1518
|
|
|
1398
1519
|
/** Adoptions the manager requested and no tick has answered yet, completed from this tick's fleet reading. */
|
|
1399
|
-
export function completeAdoptions(board, fleet, settings, report) {
|
|
1520
|
+
export function completeAdoptions(board, fleet, settings, report, lanes = null) {
|
|
1400
1521
|
const pending = view(board, db => db.prepare(`SELECT e.task_id, e.payload FROM task_events e WHERE e.kind = 'dispatch_held'
|
|
1401
1522
|
AND json_extract(e.payload, '$.reason') = 'adoption requested'
|
|
1402
1523
|
AND e.id = (SELECT MAX(id) FROM task_events l WHERE l.task_id = e.task_id AND l.kind IN ('dispatch_held','dispatch_released','session_adopted','adoption_failed'))`).all());
|
|
1403
1524
|
for (const row of pending) {
|
|
1404
1525
|
const asked = json(row.payload, {}) ?? {};
|
|
1405
1526
|
try {
|
|
1406
|
-
const got = adoptSession(board, row.task_id, asked.session, { workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null });
|
|
1527
|
+
const got = adoptSession(board, row.task_id, asked.session, { workspace: asked.workspace ?? null, assignee: asked.assignee ?? null, role: asked.role ?? null, ttl: settings.ttl, fleet: fleet?.ok ? fleet : null , lanes});
|
|
1407
1528
|
(report.adopted ??= []).push([row.task_id, got.address]);
|
|
1408
1529
|
} catch (error) {
|
|
1409
1530
|
const message = String(error.message).slice(-400);
|
|
@@ -1413,7 +1534,7 @@ export function completeAdoptions(board, fleet, settings, report) {
|
|
|
1413
1534
|
}
|
|
1414
1535
|
}
|
|
1415
1536
|
|
|
1416
|
-
function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null } = {}) {
|
|
1537
|
+
function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
|
|
1417
1538
|
fleet ??= observeFleet();
|
|
1418
1539
|
if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
|
|
1419
1540
|
const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
|
|
@@ -1433,10 +1554,21 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1433
1554
|
dir = nativeSessions(host, harness).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
|
|
1434
1555
|
}
|
|
1435
1556
|
if (!dir && found.pane) {
|
|
1436
|
-
const r =
|
|
1557
|
+
const r = runtimeDoor(normalMachine(host)===localMachine()?null:host,'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1437
1558
|
dir = (json(r.stdout.trim(), []) ?? []).find((p) => p.pane === found.pane)?.cwd ?? null;
|
|
1438
1559
|
}
|
|
1439
|
-
|
|
1560
|
+
// Rule 2: a session working a card role is that role's agent, however it started; adopted, it joins the agent as a
|
|
1561
|
+
// launch does, and its recorded profile is compared with the role's intent (rule 4).
|
|
1562
|
+
const entry0 = workflowFor(board.root), card0 = view(board, (db) => getTask(db, id));
|
|
1563
|
+
const spec0 = view(board, (db) => runSpec(entry0, entry0.workflow.verbs?.adopt?.[0]?.target ?? 'running', card0));
|
|
1564
|
+
const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[spec0?.role] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
|
|
1565
|
+
const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
|
|
1566
|
+
let agent = null;
|
|
1567
|
+
if (lane && !team.offline) {
|
|
1568
|
+
try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: spec0?.role ?? 'implementer', sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
|
|
1569
|
+
catch (error) { if (!teamsUnavailable(error)) throw error; }
|
|
1570
|
+
}
|
|
1571
|
+
const adopted = tx(board, (db) => {
|
|
1440
1572
|
const task = getTask(db, id);
|
|
1441
1573
|
if (!task) throw new Error(`no card ${id}`);
|
|
1442
1574
|
if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
|
|
@@ -1459,16 +1591,68 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1459
1591
|
if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
|
|
1460
1592
|
}
|
|
1461
1593
|
event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
|
|
1462
|
-
|
|
1594
|
+
// the manager's adoption is the workflow's own `adopt` verb where it declares one, sent as the manager; else its
|
|
1595
|
+
// dispatch start, which the platform sends for a verified adoption
|
|
1596
|
+
if (!beforeRun) { const verb = entry.workflow.verbs?.adopt || !entry.workflow.run ? 'adopt' : entry.workflow.dispatch.start;
|
|
1597
|
+
fire(db, board, entry, id, verb, { roles: [verb === 'adopt' ? 'manager' : 'platform'], payload: { lock: claimer() } }); }
|
|
1463
1598
|
const runId = getTask(db, id).current_run_id;
|
|
1464
1599
|
if (!runId) throw new Error(`${id} waits on parents that are not done`);
|
|
1465
|
-
const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now()
|
|
1600
|
+
const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now(),
|
|
1601
|
+
...(agent ? { agent_id: agent.identity.id, agent_org: team.org, agent_context: context ?? null,
|
|
1602
|
+
agent_profile_intent: { ...structuredClone(agent.intended.resolved), worker: { ...agent.intended.resolved?.worker, harness: lane.harness, model: lane.model ?? null, cwd: dir ?? task.workspace_path ?? null } } } : {}) };
|
|
1466
1603
|
setRunSession(db, runId, { ...(currentRun(db, id)?.metadata?.supercode ?? {}), ...session, slot: currentRun(db, id)?.metadata?.supercode?.slot ?? beforeSession?.slot ?? expected?.session ?? 'worker', close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null, missing_since: null });
|
|
1467
1604
|
event(db, id, 'session_adopted', { session_id: sessionId, machine: host, pane: session.pane, address: found.address, mode: 'adopt', role: currentRun(db, id)?.metadata?.supercode?.role }, runId);
|
|
1468
1605
|
subscribeIn(db, id, found.address, 'worker');
|
|
1469
1606
|
for (const p of db.prepare('SELECT parent_id FROM task_links WHERE child_id = ?').all(id)) subscribeIn(db, p.parent_id, found.address, 'dependency');
|
|
1470
|
-
return { ...session, workspace: dir };
|
|
1607
|
+
return { ...session, run: runId, workspace: dir };
|
|
1471
1608
|
});
|
|
1609
|
+
if (agent) recordAgentRun(board.root, team.org, agent.identity.id, { session: `${harness}:${sessionId}`, machine: host, run: adopted.run });
|
|
1610
|
+
return adopted;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/** A manager/creator's manual native resume rebinds its existing run; this opens no process. */
|
|
1614
|
+
function reconnectContinuedSessions(board, fleet, entry, report) {
|
|
1615
|
+
if (!monitorsRuns(entry) || !fleet.ok) return;
|
|
1616
|
+
const rows = view(board, db => db.prepare(`SELECT r.* FROM task_runs r JOIN tasks t ON t.id=r.task_id
|
|
1617
|
+
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)
|
|
1618
|
+
AND r.ended_at IS NOT NULL`).all());
|
|
1619
|
+
const panes = new Map();
|
|
1620
|
+
for (const row of rows) {
|
|
1621
|
+
const saved = json(row.metadata,{})?.supercode;
|
|
1622
|
+
if (!saved?.session_id || saved.forgotten || saved.close_transferred_to) continue;
|
|
1623
|
+
if (!saved.monitor_held && !['crashed','closed','reclaimed','failed'].includes(row.outcome)) continue;
|
|
1624
|
+
const live = fleet.sessions?.find(s => s.address === addressOf(saved) && s.state === 'running' && s.pane);
|
|
1625
|
+
if (!live) continue;
|
|
1626
|
+
if (!panes.has(saved.machine)) {
|
|
1627
|
+
const answer = doorOn(saved.machine,'harness.v1.panes.list',{}, {timeout:60000});
|
|
1628
|
+
const list = answer.code === 0 ? json(answer.stdout,null) : null;
|
|
1629
|
+
panes.set(saved.machine,Array.isArray(list) ? new Set(list.filter(p=>!p.dead).map(p=>p.pane)) : null);
|
|
1630
|
+
}
|
|
1631
|
+
const pane = live.pane;
|
|
1632
|
+
if (!pane || !panes.get(saved.machine)?.has(pane)) continue;
|
|
1633
|
+
// A surviving timed-out process is not a manual restart. It needs a new pane.
|
|
1634
|
+
if (pane === saved.pane && !saved.closed_at && !['crashed','closed','reclaimed'].includes(row.outcome)) continue;
|
|
1635
|
+
tx(board, db => {
|
|
1636
|
+
const task = getTask(db,row.task_id), scope = scopeOf(db,board,task,{params:entry.params});
|
|
1637
|
+
const automaticPause = scope.card.paused?.reason?.startsWith('its session ended cleanly (');
|
|
1638
|
+
if (task.current_run_id || (scope.card.paused && !automaticPause) || scope.card.blocked_by.length || scope.card.archived || statusOf(db,task) !== saved.status) return;
|
|
1639
|
+
if (automaticPause) db.prepare('UPDATE supercode_cards SET paused=NULL WHERE task_id=?').run(row.task_id);
|
|
1640
|
+
const session = { ...saved,pane,mode:'manual-continuation',state:entry.workflow.run?.initial ?? 'active',monitor_held:false,
|
|
1641
|
+
prior_intervals:[...(saved.prior_intervals??[]),{ended_at:row.ended_at,outcome:row.outcome,error:row.error}],
|
|
1642
|
+
launch_pending:false,launch_deferred:false,close_requested:null,closed_at:null,close_confirmed_at:null,close_error:null,missing_since:null,seen_at:now(),launch_key:null,
|
|
1643
|
+
stopped_seconds:Number(saved.stopped_seconds??0) + Math.max(0,now()-row.ended_at) };
|
|
1644
|
+
// Preserve the ended interval as evidence before reopening supervision on the same run.
|
|
1645
|
+
event(db,row.task_id,'session_continued',{session_id:saved.session_id,from_pane:saved.pane,pane,prior_outcome:row.outcome,prior_ended_at:row.ended_at,incident:saved.incident??null},row.id);
|
|
1646
|
+
db.prepare("UPDATE task_runs SET ended_at=NULL,status='running',outcome=NULL,error=NULL,metadata=?,claim_lock=?,claim_expires=? WHERE id=?")
|
|
1647
|
+
.run(JSON.stringify({...json(row.metadata,{}),supercode:session}),claimer(),now()+900,row.id);
|
|
1648
|
+
db.prepare('UPDATE tasks SET current_run_id=?,claim_lock=?,claim_expires=? WHERE id=?').run(row.id,claimer(),now()+900,row.task_id);
|
|
1649
|
+
projectStatus(db,board,entry,row.task_id);
|
|
1650
|
+
event(db,row.task_id,'dispatch_released',{reason:'verified manual continuation',session:addressOf(session),incident:saved.incident??null});
|
|
1651
|
+
subscribeIn(db,row.task_id,addressOf(session),'worker');
|
|
1652
|
+
for (const sub of db.prepare('SELECT task_id FROM supercode_cards WHERE subtask_of=?').all(row.task_id)) subscribeIn(db,sub.task_id,addressOf(session),'worker');
|
|
1653
|
+
(report.continued ??= []).push([row.task_id,addressOf(session),pane]);
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1472
1656
|
}
|
|
1473
1657
|
|
|
1474
1658
|
// ------------------------------------------------------------------ notify
|
|
@@ -1599,12 +1783,6 @@ async function notify(board, fleet, report, entry) {
|
|
|
1599
1783
|
// a worker whose run has ended is not woken by a notice (a second session would start working the card beside
|
|
1600
1784
|
// the one the next run gets): the notice is filed in its mailbox, read when it next works
|
|
1601
1785
|
const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
|
|
1602
|
-
// An idle Claude session cannot hold a notice in its mailbox (it starts a turn for every message it is given), so
|
|
1603
|
-
// a notice that must not wake it waits, unsent, for the session's next work.
|
|
1604
|
-
if (!working && fleet.ok && fleet.sessions.some((s) => s.address === sub.chat_id && s.harness === 'claude-code' && s.status === 'idle')) {
|
|
1605
|
-
report.notices_waiting = (report.notices_waiting ?? 0) + 1;
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
1786
|
let cursor = Number(sub.last_event_id);
|
|
1609
1787
|
let failed = null;
|
|
1610
1788
|
try {
|
|
@@ -1623,7 +1801,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
1623
1801
|
} else text = noticeOf(entry, role, ev.kind, task, p);
|
|
1624
1802
|
if (text) {
|
|
1625
1803
|
const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
|
|
1626
|
-
const receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
|
|
1804
|
+
const receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: String(sub.chat_id).includes(':claude-code:') ? false : !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
|
|
1627
1805
|
// A notice the recipient's mailbox took is filed: the machine delivers it at the session's next turn, so the
|
|
1628
1806
|
// board does not send it again.
|
|
1629
1807
|
if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
|
|
@@ -1873,6 +2051,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
|
|
|
1873
2051
|
|
|
1874
2052
|
/** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
|
|
1875
2053
|
export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
|
|
2054
|
+
eventFleetRoot = root;
|
|
1876
2055
|
const loaded = await loadWorkflow(root, { fresh: true });
|
|
1877
2056
|
const entry = { ...loaded, params: { ...loaded.params, ...params } };
|
|
1878
2057
|
const fleet = Object.values(lanes).some((l) => l.surface === 'pane') ? observeFleet() : { ok: false, alive: new Set(), online: new Set(), sessions: [], error: 'no pane lanes' };
|
|
@@ -1901,8 +2080,8 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
|
|
|
1901
2080
|
function noCodeReviewDue(board, id) {
|
|
1902
2081
|
return view(board, (db) => {
|
|
1903
2082
|
if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
|
|
1904
|
-
const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass') ORDER BY id DESC LIMIT 1").get(id);
|
|
1905
|
-
return !last || last.kind
|
|
2083
|
+
const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass','review_voided') ORDER BY id DESC LIMIT 1").get(id);
|
|
2084
|
+
return !last || ['changes_requested','review_voided'].includes(last.kind);
|
|
1906
2085
|
});
|
|
1907
2086
|
}
|
|
1908
2087
|
|
|
@@ -1978,6 +2157,7 @@ function capMax(cap, group, entry, lanes) {
|
|
|
1978
2157
|
* version-checked card claims; no dispatcher file lock. Answers one report per board.
|
|
1979
2158
|
*/
|
|
1980
2159
|
export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
|
|
2160
|
+
eventFleetRoot = root;
|
|
1981
2161
|
// a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
|
|
1982
2162
|
const deployment = deploymentReceipt ? workflowRevision(root) : null;
|
|
1983
2163
|
const loaded = await loadWorkflow(root, { fresh: true });
|
|
@@ -2008,16 +2188,14 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2008
2188
|
// where the tick's time goes, phase by phase (ms), printed by `serve --verbose` and in `--json`
|
|
2009
2189
|
let mark = Date.now();
|
|
2010
2190
|
const phase = (name) => { const t = Date.now(); report.ms[name] = t - mark; mark = t; };
|
|
2011
|
-
|
|
2012
|
-
const subscription = sc(['teams','machines','subscribe',boardAddress(board.label),'--json']);
|
|
2013
|
-
if (subscription.code !== 0) report.failed.push(['machine_events',(subscription.stderr || subscription.stdout).trim().slice(-200)]);
|
|
2014
|
-
}
|
|
2191
|
+
|
|
2015
2192
|
readMailbox(board, boardAddress(board.label), report); phase('mailbox');
|
|
2016
2193
|
await refreshRelationAnswers(board); phase('answers');
|
|
2017
2194
|
const effects = [];
|
|
2018
|
-
completeAdoptions(board, fleet, settings, report); reconcileLaunches(board, fleet, report, entry, effects, settings, lanes); supervise(board, fleet, settings, report, entry, effects); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
|
|
2195
|
+
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); await runEffects(board, effects.splice(0), { retryNotices: true }); closeRequested(board, report); phase('supervise');
|
|
2019
2196
|
report.unsettled = [];
|
|
2020
2197
|
report.promoted = recomputeReady(board, effects, report.unsettled);
|
|
2198
|
+
tx(board,db=>auditEndedRuns(db,board,entry));
|
|
2021
2199
|
if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
|
|
2022
2200
|
await sendBoardMessages(board, report);
|
|
2023
2201
|
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|