@volter/supercode-orchestrator 0.4.159 → 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 +304 -124
- 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,12 +133,31 @@ 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) {
|
|
149
156
|
if (offlineMachine(machine)) throw unavailable(`native liveness unavailable: machine ${normalMachine(machine)} is offline`);
|
|
150
|
-
const
|
|
157
|
+
const args = normalMachine(machine) === localMachine()
|
|
158
|
+
? ['teams', 'rpc', 'sessions.discover', JSON.stringify({ harnesses: [harness], limit: 2000 })]
|
|
159
|
+
: ['discover', '--on', machine, '--harness', harness, '--limit', '2000'];
|
|
160
|
+
const result = sc(args, { timeout: 120_000 });
|
|
151
161
|
const page = result.code === 0 ? lastJson(result.stdout) : null;
|
|
152
162
|
if (!Array.isArray(page?.sessions)) throw unavailable(`native liveness unavailable on ${machine}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
|
|
153
163
|
return page.sessions;
|
|
@@ -221,7 +231,6 @@ export function lanesOf(profiles = {}) {
|
|
|
221
231
|
return out;
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
const onMachine = machine => normalMachine(machine ?? localMachine()) === localMachine() ? [] : ['--machine', machine];
|
|
225
234
|
// The latest presence reading (observeFleet). A machine it reads as offline is not asked: each remote call would
|
|
226
235
|
// wait out its timeout, and one offline machine held every close and launch on this one.
|
|
227
236
|
let presenceReading = null;
|
|
@@ -229,45 +238,24 @@ const offlineMachine = (machine) => {
|
|
|
229
238
|
const m = normalMachine(machine ?? localMachine());
|
|
230
239
|
return m !== localMachine() && presenceReading?.known === true && !presenceReading.online.has(m);
|
|
231
240
|
};
|
|
232
|
-
/**
|
|
233
|
-
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)
|
|
234
243
|
? { code: 1, stdout: '', stderr: `machine ${normalMachine(machine)} is offline` }
|
|
235
|
-
:
|
|
244
|
+
: runtimeDoor(normalMachine(machine ?? localMachine()) === localMachine() ? null : machine, method, params, options);
|
|
236
245
|
const unavailable = message => Object.assign(new Error(message), { launchDeferred: true });
|
|
237
246
|
|
|
238
247
|
// ------------------------------------------------------------------ supervise
|
|
239
248
|
|
|
240
249
|
function supervise(board, fleet, settings, report, entry, effects) {
|
|
241
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();
|
|
242
|
-
//
|
|
243
|
-
|
|
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.
|
|
244
253
|
const exits = new Map();
|
|
245
|
-
const native = new Map(); fleet.native = new Map();
|
|
246
254
|
for (const { id } of rows) {
|
|
247
255
|
const session = currentRun(board.db, id)?.metadata?.supercode;
|
|
248
|
-
if (!session?.
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
try { native.set(key, nativeSessions(session.machine, session.harness)); }
|
|
252
|
-
catch (error) { native.set(key, null); report.failed.push([id, error.message]); }
|
|
253
|
-
}
|
|
254
|
-
fleet.native.set(addressOf(session), native.get(key) ? nativeReading(native.get(key), session) : { alive: null });
|
|
255
|
-
}
|
|
256
|
-
for (const { id } of rows) {
|
|
257
|
-
const s = currentRun(board.db, id)?.metadata?.supercode;
|
|
258
|
-
if (!s?.pane || observed.has(s.machine)) continue;
|
|
259
|
-
const r = scOn(s.machine, ['teams', 'panes', 'ls'], { timeout: 60_000 });
|
|
260
|
-
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
261
|
-
observed.set(s.machine, Array.isArray(list) ? new Set(list.filter(p => !p.dead).map((p) => p.pane)) : null);
|
|
262
|
-
if (!Array.isArray(list)) report.failed.push([id, `pane liveness unavailable; retaining run: ${(r.stderr || r.stdout).trim().slice(-200)}`]);
|
|
263
|
-
}
|
|
264
|
-
for (const {id} of rows) {
|
|
265
|
-
const session=currentRun(board.db,id)?.metadata?.supercode;
|
|
266
|
-
const panes=observed.get(session?.machine);
|
|
267
|
-
if(session?.launch_key && panes instanceof Set && !panes.has(session.pane)) {
|
|
268
|
-
try { const receipt=launchReceipt(board,session);if(receipt?.live===false&&receipt.exit)exits.set(id,receipt.exit); }
|
|
269
|
-
catch(error){report.failed.push([id,`exit receipt unavailable; retaining liveness observation: ${error.message}`]);}
|
|
270
|
-
}
|
|
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}`]); }
|
|
271
259
|
}
|
|
272
260
|
for (const { id } of rows) {
|
|
273
261
|
tx(board, (db) => {
|
|
@@ -275,20 +263,20 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
275
263
|
if (!task || (entry.workflow.run ? !task.current_run_id : task.status !== 'running')) return;
|
|
276
264
|
const runRow = currentRun(db, id);
|
|
277
265
|
let session = runRow?.metadata?.supercode ?? null;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (found && found !== session.pane) {
|
|
283
|
-
session = { ...session, pane: found, seen_at: now(), missing_since: null, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
|
|
284
|
-
setRunSession(db, runRow.id, session);
|
|
285
|
-
event(db, id, 'session_rebound', { session_id: session.session_id, pane: found }, runRow.id);
|
|
286
|
-
}
|
|
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);
|
|
287
270
|
}
|
|
288
271
|
const t = now();
|
|
289
272
|
// what the dispatcher saw becomes a platform event; the card's workflow says what it does
|
|
290
273
|
const observe = (name, payload, bucket) => {
|
|
291
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
|
+
}
|
|
292
280
|
const r = fire(db, board, entry, id, name, { roles: ['platform'], payload });
|
|
293
281
|
effects.push(...r.effects);
|
|
294
282
|
if (bucket) report[bucket].push(id);
|
|
@@ -316,8 +304,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
316
304
|
// Availability is the same retained linked/unlinked feed subscribers hear.
|
|
317
305
|
// A failed stream is unknown, not evidence that every remote machine left.
|
|
318
306
|
if (session?.session_id && session.surface !== 'headless') {
|
|
319
|
-
const
|
|
320
|
-
const reachable = panes instanceof Set || fleet.online.has(session.machine);
|
|
307
|
+
const reachable = fleet.online.has(session.machine);
|
|
321
308
|
if (!reachable && !fleet.presenceKnown) return;
|
|
322
309
|
if (!reachable) {
|
|
323
310
|
const next = {...session, offline_since: session.offline_since ?? t};
|
|
@@ -338,8 +325,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
338
325
|
}
|
|
339
326
|
}
|
|
340
327
|
// Hermes's orphan reconciliation: a running card whose claim bookkeeping is broken, with nothing live behind it
|
|
341
|
-
const
|
|
342
|
-
const paneLive = paneReading instanceof Set ? paneReading.has(session.pane) : null;
|
|
328
|
+
const paneLive = session?.session_id ? sessionAlive(fleet,session) : null;
|
|
343
329
|
const live = session?.surface === 'headless' ? pidAlive(session.pid) : session?.pane ? paneLive !== false : session?.session_id ? (fleet.ok ? sessionAlive(fleet, session) !== false : true) : false;
|
|
344
330
|
// (whether it is reconciled is the workflow's: its `orphaned` transitions and their guard)
|
|
345
331
|
if ((!task.claim_lock || !task.claim_expires) && !live) {
|
|
@@ -375,6 +361,7 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
375
361
|
// follows, and it is not a crash to restart
|
|
376
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 };
|
|
377
363
|
if (!exit.failed) {
|
|
364
|
+
if (monitorsRuns(entry)) { observe('session_closed', {...ended,exit},'crashed'); return; }
|
|
378
365
|
try {
|
|
379
366
|
const r = fire(db, board, entry, id, 'session_closed', { roles: ['platform'], payload: ended });
|
|
380
367
|
effects.push(...r.effects); (report.closed_by_hand ??= []).push(id);
|
|
@@ -384,11 +371,10 @@ function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
384
371
|
observe('exited', ended, 'crashed');
|
|
385
372
|
return;
|
|
386
373
|
}
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
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);
|
|
392
378
|
const young = t - Number(session.launched_at ?? 0) < Number(limits.grace ?? settings.grace);
|
|
393
379
|
const missing = !alive && machineUp && !young;
|
|
394
380
|
const next = { ...session };
|
|
@@ -447,7 +433,7 @@ function stopSession(board, session) {
|
|
|
447
433
|
return;
|
|
448
434
|
}
|
|
449
435
|
if (session.pane) {
|
|
450
|
-
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)});
|
|
451
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)}`);
|
|
452
438
|
}
|
|
453
439
|
}
|
|
@@ -534,6 +520,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
534
520
|
const stop = paused(board.root);
|
|
535
521
|
if (stop) { report.paused = stop; return; }
|
|
536
522
|
const { workflow, params } = entry;
|
|
523
|
+
if (!dry) tx(board, db => auditEndedRuns(db, board, entry));
|
|
537
524
|
const d = workflow.dispatch;
|
|
538
525
|
const pressure = memoryPressure();
|
|
539
526
|
const tickState = { local_started: 0 };
|
|
@@ -630,7 +617,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
630
617
|
} catch (error) {
|
|
631
618
|
if (error.launchPending) { report.queued.push([task.id, error.message]); continue; }
|
|
632
619
|
const payload = { error: String(error.message).slice(-500), ...(error.resumeFailed ? { resume_failed: true, ...error.resumeFailed } : {}) };
|
|
633
|
-
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 */ }
|
|
634
621
|
report.failed.push([task.id, String(error.message).slice(-300)]);
|
|
635
622
|
}
|
|
636
623
|
}
|
|
@@ -653,7 +640,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
653
640
|
const where = whereOf(board, task);
|
|
654
641
|
// a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
|
|
655
642
|
const arcContext = task.subtask_of ? view(board, (db) => cardContext(db, task.subtask_of)) : '';
|
|
656
|
-
|
|
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) });
|
|
657
644
|
// the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
|
|
658
645
|
// is a template, so a step can ask for isolation by naming a new one
|
|
659
646
|
const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
|
|
@@ -665,12 +652,57 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
665
652
|
// the model it runs is recorded with the session, for caps that count by model
|
|
666
653
|
const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
|
|
667
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}`;}
|
|
668
682
|
const result = surface === 'pane'
|
|
669
683
|
? launchPane(board, task, runId, lane, fleet, { input: prompt, slot, meta })
|
|
670
684
|
: launchHeadless(board, task, runId, lane, settings, { prompt, skills: spec.skills ?? [], meta });
|
|
671
|
-
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
|
+
}
|
|
672
697
|
return result;
|
|
673
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
|
+
});
|
|
674
706
|
if(error.launchDeferred) {
|
|
675
707
|
tx(board,db=>{
|
|
676
708
|
const previous=currentRun(db,id)?.metadata?.supercode;
|
|
@@ -685,22 +717,33 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
685
717
|
|
|
686
718
|
/** Start one card now by a person's word (a triage card's session, a manager's start): the workflow's start event. */
|
|
687
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;
|
|
688
723
|
const entry = await loadWorkflow(board.root);
|
|
689
724
|
const fleet = observeFleet();
|
|
690
725
|
const before = getTaskNow(board, id), checked = checkWorkspace(board, before);
|
|
691
726
|
const moved = tx(board, (db) => {
|
|
692
727
|
assertWorkspaceOwner(db, before, checked);
|
|
693
728
|
if (checked) db.prepare('UPDATE supercode_cards SET workspace_root=? WHERE task_id=?').run(checked.mainRoot, id);
|
|
694
|
-
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.');
|
|
695
730
|
event(db, id, 'dispatch_released', { reason: 'explicit start' });
|
|
696
|
-
|
|
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;
|
|
697
735
|
});
|
|
698
736
|
const task = getTaskNow(board, id);
|
|
699
737
|
const spec = runSpec(entry, moved.to, task);
|
|
700
738
|
const profile = spec ? view(board, (db) => evaluate(entry.workflow.roles?.[spec.role] ?? 'card.assignee', scopeOf(db, board, task, { params: entry.params }))) : null;
|
|
701
739
|
const lane = profile ? lanes[profile] : null;
|
|
702
740
|
if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
|
|
703
|
-
|
|
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
|
+
}
|
|
704
747
|
}
|
|
705
748
|
|
|
706
749
|
/** Reconcile durable close intent and ended runs, including old unverified closed_at records.
|
|
@@ -737,7 +780,9 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
737
780
|
(report.closed ??= []).push({ task_id: row.task_id, ...session, receipt });
|
|
738
781
|
continue;
|
|
739
782
|
}
|
|
740
|
-
|
|
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 }; }
|
|
741
786
|
else {
|
|
742
787
|
const error = `close deferred: ${receiptError ?? `awaiting terminal launch receipt ${session.launch_key} (${receipt?.status ?? 'unknown'})`}`;
|
|
743
788
|
tx(board, (db) => {
|
|
@@ -748,6 +793,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
748
793
|
continue;
|
|
749
794
|
}
|
|
750
795
|
}
|
|
796
|
+
if (session?.monitor_held && !session.close_requested) continue; // only an explicit close action ends this process
|
|
751
797
|
if ((!session?.pane && session?.surface !== 'native') || session.close_transferred_to || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
|
|
752
798
|
if (caller && session.session_id === caller) continue; // let the command that ended its own run return
|
|
753
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());
|
|
@@ -766,7 +812,7 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
766
812
|
}
|
|
767
813
|
} else {
|
|
768
814
|
if (!panes.has(machine)) {
|
|
769
|
-
const r =
|
|
815
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000, as: boardAddress(board.label) });
|
|
770
816
|
const list = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
771
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)}` });
|
|
772
818
|
}
|
|
@@ -826,6 +872,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
826
872
|
threshold_seconds: settings.idleQuiet, turns_before: Number(session.goal_turns ?? 0),
|
|
827
873
|
};
|
|
828
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;
|
|
829
878
|
const observation = { ...reading, decision, turns_after: reading.turns_before, ...extra };
|
|
830
879
|
const patch = { idle_observation: observation };
|
|
831
880
|
if (decision === 'below_threshold') patch.idle_below_threshold = observation;
|
|
@@ -993,9 +1042,9 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
|
|
|
993
1042
|
|
|
994
1043
|
/** A transport error is not a negative launch receipt. The key remains owned by its run. */
|
|
995
1044
|
function launchReceipt(board, session) {
|
|
996
|
-
const r =
|
|
1045
|
+
const r = doorOn(session.machine, 'harness.v1.launches.get',{key:session.launch_key},
|
|
997
1046
|
{ timeout: 30_000, as: boardAddress(board.label) });
|
|
998
|
-
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)}`);
|
|
999
1048
|
if (r.stdout.trim() === 'null') return null;
|
|
1000
1049
|
const receipt = lastJson(r.stdout);
|
|
1001
1050
|
if (!receipt || receipt.key !== session.launch_key) throw unavailable(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
|
|
@@ -1003,7 +1052,7 @@ function launchReceipt(board, session) {
|
|
|
1003
1052
|
}
|
|
1004
1053
|
|
|
1005
1054
|
function cancelUnissuedLaunch(board, session) {
|
|
1006
|
-
const result =
|
|
1055
|
+
const result = doorOn(session.machine, 'harness.v1.launches.cancel', {key:session.launch_key},
|
|
1007
1056
|
{ timeout: 30_000, as: boardAddress(board.label) });
|
|
1008
1057
|
if (result.code !== 0) throw unavailable(`launch cancellation unavailable on ${session.machine}; retain ${session.launch_key}: ${(result.stderr || result.stdout).trim().slice(-300)}`);
|
|
1009
1058
|
const receipt = lastJson(result.stdout);
|
|
@@ -1031,7 +1080,8 @@ function retireUnissuedLaunch(board, row, session, receipt, { entry = null, effe
|
|
|
1031
1080
|
forgotten: saved.mode === 'fresh' ? true : Boolean(saved.forgotten) });
|
|
1032
1081
|
event(db, row.task_id, 'session_launch_cancelled', { key: session.launch_key, machine: session.machine, receipt }, row.id);
|
|
1033
1082
|
if (!current.ended_at) {
|
|
1034
|
-
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});
|
|
1035
1085
|
collected.push(...moved.effects);
|
|
1036
1086
|
}
|
|
1037
1087
|
event(db, row.task_id, 'session_closed', { key: session.launch_key, machine: session.machine, reason: 'launch not issued', receipt }, row.id);
|
|
@@ -1062,6 +1112,7 @@ function bindLaunch(board, row, session, pane, sid) {
|
|
|
1062
1112
|
|
|
1063
1113
|
/** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
|
|
1064
1114
|
export function reconcileCallerLaunch(board, sid) {
|
|
1115
|
+
eventFleetRoot = board.root;
|
|
1065
1116
|
if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
|
|
1066
1117
|
WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
|
|
1067
1118
|
const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
|
|
@@ -1086,7 +1137,63 @@ export function reconcileCallerLaunch(board, sid) {
|
|
|
1086
1137
|
}
|
|
1087
1138
|
|
|
1088
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
|
+
}
|
|
1089
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
|
|
1090
1197
|
for(const {id,started_at} of deferred) {
|
|
1091
1198
|
if (now() - started_at >= settings.launchWindow) continue;
|
|
1092
1199
|
try {
|
|
@@ -1098,7 +1205,7 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
|
|
|
1098
1205
|
if(session)report.started.push([id,session.address??session.session_id,session.mode]);
|
|
1099
1206
|
}catch(error){
|
|
1100
1207
|
if(error.launchPending){report.queued.push([id,error.message]);continue;}
|
|
1101
|
-
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);
|
|
1102
1209
|
report.failed.push([id,error.message]);
|
|
1103
1210
|
}
|
|
1104
1211
|
}
|
|
@@ -1127,7 +1234,9 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
|
|
|
1127
1234
|
const moved = tx(board, (db) => {
|
|
1128
1235
|
if (getTask(db, row.task_id)?.current_run_id !== row.id) return null;
|
|
1129
1236
|
setRunSession(db, row.id, { launch_pending: false });
|
|
1130
|
-
|
|
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 });
|
|
1131
1240
|
});
|
|
1132
1241
|
if (moved) effects.push(...moved.effects);
|
|
1133
1242
|
}
|
|
@@ -1150,11 +1259,13 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1150
1259
|
task = { ...task, workspace_path: made };
|
|
1151
1260
|
}
|
|
1152
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};
|
|
1153
1263
|
const tail = model ? ['--model', model] : [];
|
|
1154
1264
|
if (lane.harness === 'codex' && lane.reasoning_effort) tail.push('-c', `model_reasoning_effort=${JSON.stringify(lane.reasoning_effort)}`);
|
|
1155
1265
|
const harness = HARNESSES[lane.harness] ? lane.harness : 'claude-code';
|
|
1156
1266
|
const how = HARNESSES[harness];
|
|
1157
|
-
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}`])];
|
|
1158
1269
|
// the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
|
|
1159
1270
|
// a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
|
|
1160
1271
|
// session starts (never two processes on one session)
|
|
@@ -1168,7 +1279,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1168
1279
|
if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
|
|
1169
1280
|
const fresh = observeFleet();
|
|
1170
1281
|
if (!fresh.ok) throw unavailable(`cannot check for a manual continuation of ${prev.session_id}: ${fresh.error}`);
|
|
1171
|
-
const r =
|
|
1282
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1172
1283
|
const listed = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
1173
1284
|
if (!Array.isArray(listed)) throw unavailable(`cannot read panes before continuing ${prev.session_id}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1174
1285
|
const pane = continuationPane(fresh, prev, new Set(listed.filter(p => !p.dead).map((p) => p.pane)));
|
|
@@ -1183,6 +1294,9 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1183
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'}`);
|
|
1184
1295
|
}
|
|
1185
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`);
|
|
1186
1300
|
launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
|
|
1187
1301
|
tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
|
|
1188
1302
|
session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
|
|
@@ -1233,7 +1347,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1233
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`);
|
|
1234
1348
|
if (!prev.closed_at && !prev.close_requested && (prev.pane ? paneLive : sessionAlive(fleet, prev))) {
|
|
1235
1349
|
// the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
|
|
1236
|
-
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;
|
|
1237
1351
|
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
1238
1352
|
// a kept session is told what this run is for (its card came back to it, with what happened since)
|
|
1239
1353
|
tx(board, (db) => setRunSession(db, runId, { ...session, ...meta }));
|
|
@@ -1249,6 +1363,15 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1249
1363
|
}
|
|
1250
1364
|
}
|
|
1251
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`);
|
|
1252
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
|
|
1253
1376
|
// its next round gets that round's prompt, its arc's context with it), not only that it was resumed
|
|
1254
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.`;
|
|
@@ -1301,7 +1424,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
1301
1424
|
|
|
1302
1425
|
/** A machine reading, not a missing catalog entry, establishes whether a pane is alive. */
|
|
1303
1426
|
function panePresent(machine, pane) {
|
|
1304
|
-
const r =
|
|
1427
|
+
const r = doorOn(machine, 'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1305
1428
|
const panes = r.code === 0 ? json(r.stdout.trim(), null) : null;
|
|
1306
1429
|
if (!Array.isArray(panes)) throw unavailable(`cannot establish pane ${pane} liveness on ${machine}: ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
1307
1430
|
return panes.some((p) => p.pane === pane && !p.dead);
|
|
@@ -1316,6 +1439,7 @@ function paneSurvives(machine, pane) {
|
|
|
1316
1439
|
/** Hermes's own worker argv and task environment, run on this machine; watched by its pid. */
|
|
1317
1440
|
function launchHeadless(board, task, runId, lane, settings, { prompt = null, skills: runSkills = [], meta = {} } = {}) {
|
|
1318
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};
|
|
1319
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));
|
|
1320
1444
|
// the card's own skills and the ones its status's run names (a reviewer's, for one)
|
|
1321
1445
|
const skills = [...new Set([...task.skills, ...runSkills])];
|
|
@@ -1334,7 +1458,7 @@ function launchHeadless(board, task, runId, lane, settings, { prompt = null, ski
|
|
|
1334
1458
|
env: {
|
|
1335
1459
|
SUPERCODE_BOARD_EXIT_FILE: exitFile,
|
|
1336
1460
|
...(task.goal_mode ? { HERMES_KANBAN_GOAL_MODE: '1', ...(task.goal_max_turns ? { HERMES_KANBAN_GOAL_MAX_TURNS: String(task.goal_max_turns) } : {}) } : {}),
|
|
1337
|
-
...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,
|
|
1338
1462
|
HERMES_KANBAN_RUN_ID: String(runId), HERMES_KANBAN_DB: board.path, HERMES_KANBAN_BOARD: board.slug, HERMES_SESSION_SOURCE: 'kanban',
|
|
1339
1463
|
HERMES_KANBAN_CLAIM_LOCK: claimer(), ...(prompt ? { SUPERCODE_BOARD_PROMPT: prompt } : {}), ...(task.tenant ? { HERMES_TENANT: task.tenant } : {}),
|
|
1340
1464
|
...(task.branch_name ? { HERMES_KANBAN_BRANCH: task.branch_name } : {}),
|
|
@@ -1393,14 +1517,14 @@ export function requestAdoption(board, id, who, { workspace = null, assignee = n
|
|
|
1393
1517
|
}
|
|
1394
1518
|
|
|
1395
1519
|
/** Adoptions the manager requested and no tick has answered yet, completed from this tick's fleet reading. */
|
|
1396
|
-
export function completeAdoptions(board, fleet, settings, report) {
|
|
1520
|
+
export function completeAdoptions(board, fleet, settings, report, lanes = null) {
|
|
1397
1521
|
const pending = view(board, db => db.prepare(`SELECT e.task_id, e.payload FROM task_events e WHERE e.kind = 'dispatch_held'
|
|
1398
1522
|
AND json_extract(e.payload, '$.reason') = 'adoption requested'
|
|
1399
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());
|
|
1400
1524
|
for (const row of pending) {
|
|
1401
1525
|
const asked = json(row.payload, {}) ?? {};
|
|
1402
1526
|
try {
|
|
1403
|
-
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});
|
|
1404
1528
|
(report.adopted ??= []).push([row.task_id, got.address]);
|
|
1405
1529
|
} catch (error) {
|
|
1406
1530
|
const message = String(error.message).slice(-400);
|
|
@@ -1410,7 +1534,7 @@ export function completeAdoptions(board, fleet, settings, report) {
|
|
|
1410
1534
|
}
|
|
1411
1535
|
}
|
|
1412
1536
|
|
|
1413
|
-
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 } = {}) {
|
|
1414
1538
|
fleet ??= observeFleet();
|
|
1415
1539
|
if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
|
|
1416
1540
|
const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
|
|
@@ -1427,14 +1551,24 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1427
1551
|
// the session's own working directory, as discovery records it (where `--resume` finds it), else its pane's
|
|
1428
1552
|
let dir = workspace ?? native?.row?.cwd;
|
|
1429
1553
|
if (!dir) {
|
|
1430
|
-
|
|
1431
|
-
dir = (json(r.stdout.trim(), {})?.sessions ?? []).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
|
|
1554
|
+
dir = nativeSessions(host, harness).find((x) => x.locator?.session_id === sessionId)?.cwd ?? null;
|
|
1432
1555
|
}
|
|
1433
1556
|
if (!dir && found.pane) {
|
|
1434
|
-
const r =
|
|
1557
|
+
const r = runtimeDoor(normalMachine(host)===localMachine()?null:host,'harness.v1.panes.list',{}, { timeout: 60_000 });
|
|
1435
1558
|
dir = (json(r.stdout.trim(), []) ?? []).find((p) => p.pane === found.pane)?.cwd ?? null;
|
|
1436
1559
|
}
|
|
1437
|
-
|
|
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) => {
|
|
1438
1572
|
const task = getTask(db, id);
|
|
1439
1573
|
if (!task) throw new Error(`no card ${id}`);
|
|
1440
1574
|
if ((task.current_run_id ?? null) !== (beforeRun?.id ?? null)) throw new Error(`${id} changed runs during adoption; retry against its current run`);
|
|
@@ -1457,16 +1591,68 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
|
|
|
1457
1591
|
if (implementer?.session_id === sessionId) throw new Error('a reviewer must be independent of the implementer session');
|
|
1458
1592
|
}
|
|
1459
1593
|
event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
|
|
1460
|
-
|
|
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() } }); }
|
|
1461
1598
|
const runId = getTask(db, id).current_run_id;
|
|
1462
1599
|
if (!runId) throw new Error(`${id} waits on parents that are not done`);
|
|
1463
|
-
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 } } } : {}) };
|
|
1464
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 });
|
|
1465
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);
|
|
1466
1605
|
subscribeIn(db, id, found.address, 'worker');
|
|
1467
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');
|
|
1468
|
-
return { ...session, workspace: dir };
|
|
1607
|
+
return { ...session, run: runId, workspace: dir };
|
|
1469
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
|
+
}
|
|
1470
1656
|
}
|
|
1471
1657
|
|
|
1472
1658
|
// ------------------------------------------------------------------ notify
|
|
@@ -1597,12 +1783,6 @@ async function notify(board, fleet, report, entry) {
|
|
|
1597
1783
|
// a worker whose run has ended is not woken by a notice (a second session would start working the card beside
|
|
1598
1784
|
// the one the next run gets): the notice is filed in its mailbox, read when it next works
|
|
1599
1785
|
const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
|
|
1600
|
-
// An idle Claude session cannot hold a notice in its mailbox (it starts a turn for every message it is given), so
|
|
1601
|
-
// a notice that must not wake it waits, unsent, for the session's next work.
|
|
1602
|
-
if (!working && fleet.ok && fleet.sessions.some((s) => s.address === sub.chat_id && s.harness === 'claude-code' && s.status === 'idle')) {
|
|
1603
|
-
report.notices_waiting = (report.notices_waiting ?? 0) + 1;
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
1786
|
let cursor = Number(sub.last_event_id);
|
|
1607
1787
|
let failed = null;
|
|
1608
1788
|
try {
|
|
@@ -1621,7 +1801,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
1621
1801
|
} else text = noticeOf(entry, role, ev.kind, task, p);
|
|
1622
1802
|
if (text) {
|
|
1623
1803
|
const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
|
|
1624
|
-
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}` });
|
|
1625
1805
|
// A notice the recipient's mailbox took is filed: the machine delivers it at the session's next turn, so the
|
|
1626
1806
|
// board does not send it again.
|
|
1627
1807
|
if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
|
|
@@ -1871,6 +2051,7 @@ const CORRUPT = /malformed|not a database|corrupt|disk image/i;
|
|
|
1871
2051
|
|
|
1872
2052
|
/** What a tick would start on each board of the home, planned through the workflow's `dispatch`, writing nothing. */
|
|
1873
2053
|
export async function plan({ root, lanes, settings, slugs = null, params = {} }) {
|
|
2054
|
+
eventFleetRoot = root;
|
|
1874
2055
|
const loaded = await loadWorkflow(root, { fresh: true });
|
|
1875
2056
|
const entry = { ...loaded, params: { ...loaded.params, ...params } };
|
|
1876
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' };
|
|
@@ -1899,8 +2080,8 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
|
|
|
1899
2080
|
function noCodeReviewDue(board, id) {
|
|
1900
2081
|
return view(board, (db) => {
|
|
1901
2082
|
if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
|
|
1902
|
-
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);
|
|
1903
|
-
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);
|
|
1904
2085
|
});
|
|
1905
2086
|
}
|
|
1906
2087
|
|
|
@@ -1976,6 +2157,7 @@ function capMax(cap, group, entry, lanes) {
|
|
|
1976
2157
|
* version-checked card claims; no dispatcher file lock. Answers one report per board.
|
|
1977
2158
|
*/
|
|
1978
2159
|
export async function tick({ root, lanes, settings, only = null, slugs = null, dispatch = true, params = {}, deploymentReceipt = false }) {
|
|
2160
|
+
eventFleetRoot = root;
|
|
1979
2161
|
// a verb's flags (dispatch --max, --failure-limit) are params of this tick, read by the workflow as its own
|
|
1980
2162
|
const deployment = deploymentReceipt ? workflowRevision(root) : null;
|
|
1981
2163
|
const loaded = await loadWorkflow(root, { fresh: true });
|
|
@@ -2006,16 +2188,14 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2006
2188
|
// where the tick's time goes, phase by phase (ms), printed by `serve --verbose` and in `--json`
|
|
2007
2189
|
let mark = Date.now();
|
|
2008
2190
|
const phase = (name) => { const t = Date.now(); report.ms[name] = t - mark; mark = t; };
|
|
2009
|
-
|
|
2010
|
-
const subscription = sc(['teams','machines','subscribe',boardAddress(board.label),'--json']);
|
|
2011
|
-
if (subscription.code !== 0) report.failed.push(['machine_events',(subscription.stderr || subscription.stdout).trim().slice(-200)]);
|
|
2012
|
-
}
|
|
2191
|
+
|
|
2013
2192
|
readMailbox(board, boardAddress(board.label), report); phase('mailbox');
|
|
2014
2193
|
await refreshRelationAnswers(board); phase('answers');
|
|
2015
2194
|
const effects = [];
|
|
2016
|
-
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');
|
|
2017
2196
|
report.unsettled = [];
|
|
2018
2197
|
report.promoted = recomputeReady(board, effects, report.unsettled);
|
|
2198
|
+
tx(board,db=>auditEndedRuns(db,board,entry));
|
|
2019
2199
|
if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
|
|
2020
2200
|
await sendBoardMessages(board, report);
|
|
2021
2201
|
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|