@volter/supercode-orchestrator 0.5.112 → 0.5.114
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/board/ci.mjs +39 -12
- package/board/cli.mjs +12 -8
- package/board/dispatch.mjs +66 -10
- package/board/engine.mjs +16 -3
- package/board/incidents.mjs +27 -0
- package/board/published.mjs +5 -2
- package/board/session-work.mjs +19 -0
- package/board/workflow.mjs +1 -1
- package/package.json +1 -1
package/board/ci.mjs
CHANGED
|
@@ -118,7 +118,7 @@ export function withCompletionEvidence(board, id, state, fn) {
|
|
|
118
118
|
export function reviewCandidate(board, id) {
|
|
119
119
|
const row = board.db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind='review_requested' ORDER BY id DESC LIMIT 1").get(id);
|
|
120
120
|
const payload = row && JSON.parse(row.payload);
|
|
121
|
-
return payload?.ci?.pr ? payload.ci : null;
|
|
121
|
+
return payload?.ci?.pr || payload?.ci?.no_code ? payload.ci : null;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** The PRs an evidence record covers: every PR of one arc review (the first is also its top-level PR). */
|
|
@@ -215,17 +215,17 @@ export function declaredWorkBranches(db, id) {
|
|
|
215
215
|
* answers `{ waits }` saying so. One review covers them all; the caller decides the transition. */
|
|
216
216
|
export async function openReviewPullRequests(board, id) {
|
|
217
217
|
const own = await repositoryWorkspace(board, id);
|
|
218
|
-
|
|
218
|
+
const registered = registeredBranches(board, id);
|
|
219
|
+
if (!own && !registered.length) return null;
|
|
219
220
|
const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
|
|
220
|
-
const ownRepository = own
|
|
221
|
-
if (!ownRepository) return null;
|
|
222
|
-
const current = task?.branch_name ? null : own
|
|
221
|
+
const ownRepository = own?.repository ?? (own?.here ? await originOf(own.path) : null);
|
|
222
|
+
if (!ownRepository && !registered.length) return null;
|
|
223
|
+
const current = task?.branch_name ? null : own?.branch ?? (own?.here ? await run('git', ['branch', '--show-current'], own.path) : null);
|
|
223
224
|
let branch = task?.branch_name ?? (current || null);
|
|
224
|
-
const registered = registeredBranches(board, id);
|
|
225
225
|
const mine = (repository, head) => (repository === ownRepository && head === branch) || namedWorkBranch(id, head)
|
|
226
226
|
|| registered.some((r) => r.repository === repository && r.branch === head);
|
|
227
227
|
const prs = [];
|
|
228
|
-
for (const repository of new Set([ownRepository, ...registered.map((r) => r.repository), ...(await knownRepositories())])) {
|
|
228
|
+
for (const repository of new Set([ownRepository, ...registered.map((r) => r.repository), ...(await knownRepositories())].filter(Boolean))) {
|
|
229
229
|
// Discovery is shared per repository for one minute; each selected PR/head is re-read before it authorizes a move.
|
|
230
230
|
let list = pullRequestLists.get(repository);
|
|
231
231
|
if (!list || Date.now() - list.at >= 60_000) {
|
|
@@ -263,7 +263,19 @@ export async function openReviewPullRequests(board, id) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
|
|
266
|
+
// A source declaration (an implementer's or manager's card comment, engine.comment) adds merged or moved PRs that
|
|
267
|
+
// discovery cannot see; it never hides what discovery found. Only repositories the card's workspace or registered
|
|
268
|
+
// branches name count, and each declared PR is followed at its current head (GitHub's), never pinned to the
|
|
269
|
+
// declared one, so a push after the declaration is reviewed instead of failing every tick.
|
|
270
|
+
const declaration = board.db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind='commented' AND json_type(payload,'$.sources')='array' ORDER BY id DESC LIMIT 1").get(id);
|
|
271
|
+
const repositories = new Set([ownRepository, ...registered.map((r) => r.repository)].filter(Boolean).map((r) => r.toLowerCase()));
|
|
272
|
+
for (const source of declaration ? JSON.parse(declaration.payload).sources : []) {
|
|
273
|
+
if (!repositories.has(repositoryOf(source.url).toLowerCase()) || prs.some((known) => known.url === source.url)) continue;
|
|
274
|
+
const pr = await readPullRequest(source.url);
|
|
275
|
+
if (pr.isCrossRepository || !['OPEN', 'MERGED'].includes(pr.state) || (pr.state === 'OPEN' && pr.isDraft)) continue;
|
|
276
|
+
if (!prs.some((known) => known.url === pr.url)) prs.push({ url: pr.url, head: pr.headRefOid });
|
|
277
|
+
}
|
|
278
|
+
if (!prs.length) return branch || !ownRepository ? null : { waits: `the card has no work branch (${task?.branch_name ? `it declares ${task.branch_name}, its repository's default` : current ? `its checkout ${own.path} is on ${current}, its repository's default` : `its checkout ${own.path} is on no branch`}): give it one with --branch, or open its PR from a wt/${id} branch` };
|
|
267
279
|
return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
|
|
268
280
|
}
|
|
269
281
|
|
|
@@ -271,6 +283,14 @@ export async function openReviewPullRequests(board, id) {
|
|
|
271
283
|
* beyond its repository's default branch, locally or on origin, its checkout holds no uncommitted work, no PR was
|
|
272
284
|
* ever opened from it, and it registered no other branch. Answers what its review reads instead of a diff, or null when the arc carries (or may carry) code. */
|
|
273
285
|
export async function noCodeChange(board, id) {
|
|
286
|
+
const taskRow = board.db.prepare('SELECT workspace_kind FROM tasks WHERE id=?').get(id);
|
|
287
|
+
if (taskRow?.workspace_kind === 'scratch') {
|
|
288
|
+
if (board.db.prepare("SELECT 1 FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE c.subtask_of=? AND t.workspace_kind!='scratch' AND t.status!='cancelled' LIMIT 1").get(id)) return null;
|
|
289
|
+
// Scratch declares an operational disposition rather than repository code.
|
|
290
|
+
// Closed tasks/results and the retained card thread are the review source;
|
|
291
|
+
// this does not invent a verdict or assert that an external act succeeded.
|
|
292
|
+
return { pr: [], pr_head: [], branch: null, no_code: { kind: 'disposition' } };
|
|
293
|
+
}
|
|
274
294
|
const checkout = await repositoryWorkspace(board, id);
|
|
275
295
|
if (!checkout?.here) return null;
|
|
276
296
|
const own = checkout.path;
|
|
@@ -357,6 +377,7 @@ export async function inspectCompletionEvidence(board, id, entry, payload = {},
|
|
|
357
377
|
const candidate = stage === 'acceptance' ? reviewCandidate(board, id) : null;
|
|
358
378
|
if (candidate) {
|
|
359
379
|
const row = board.db.prepare("SELECT id,payload FROM task_events WHERE task_id=? AND kind='review_pass' ORDER BY id DESC LIMIT 1").get(id);
|
|
380
|
+
if (!row) throw new Error('The current candidate needs its independent verdict before acceptance.');
|
|
360
381
|
const voided = board.db.prepare("SELECT id FROM task_events WHERE task_id=? AND kind='review_voided' ORDER BY id DESC LIMIT 1").get(id);
|
|
361
382
|
if (voided && (!row || voided.id > row.id)) throw new Error('The manager voided this verdict; the board must obtain a new review before acceptance.');
|
|
362
383
|
const passed = candidatePullRequests(row && JSON.parse(row.payload)?.ci);
|
|
@@ -370,13 +391,19 @@ export async function inspectCompletionEvidence(board, id, entry, payload = {},
|
|
|
370
391
|
}
|
|
371
392
|
return state;
|
|
372
393
|
}
|
|
373
|
-
const own = await repositoryWorkspace(board, id);
|
|
374
394
|
const asked = stage === 'review_verdict' ? reviewCandidate(board, id) : null;
|
|
375
395
|
const urls = [].concat(payload.pr ?? payload.metadata?.pr ?? candidatePullRequests(asked).map((c) => c.pr.url)).filter(Boolean);
|
|
376
|
-
if (!
|
|
377
|
-
|
|
396
|
+
if (!urls.length && asked?.no_code) return { ...state, no_code: asked.no_code };
|
|
397
|
+
if (urls.some(url => typeof url !== 'string' || !/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*\/?$/.test(url))) throw new Error('Name each GitHub PR URL with --pr.');
|
|
398
|
+
// Merged source is immutable and needs no surviving workspace. Resolve an
|
|
399
|
+
// unmerged candidate's checkout as before; a broken legacy path never
|
|
400
|
+
// prevents a verdict on already merged commits.
|
|
401
|
+
const mergedOnly = urls.length && (await Promise.all(urls.map(readPullRequest))).every(pr => pr.state === 'MERGED');
|
|
402
|
+
const own = mergedOnly ? null : await repositoryWorkspace(board, id);
|
|
403
|
+
if (!own && !urls.length) {
|
|
378
404
|
return state;
|
|
379
405
|
}
|
|
406
|
+
if (!own && !mergedOnly) throw new Error('An unmerged PR candidate needs a resolved repository workspace.');
|
|
380
407
|
// A verdict with no requested PR (a scratch arc) keeps its own contract. An arc's review
|
|
381
408
|
// always names every opened PR it changed, independent of completion_ci.
|
|
382
409
|
if (stage === 'review_verdict' && !asked && !urls.length) return state;
|
|
@@ -412,7 +439,7 @@ async function verifyPullRequest(board, id, entry, own, url, supplied, stage) {
|
|
|
412
439
|
// card's own checkout, and this machine's checkout of the PR's branch when it keeps one, must also hold that head
|
|
413
440
|
// with nothing uncommitted. A card on another machine is reviewed at its pushed head: its checkout is not read from here.
|
|
414
441
|
const observed = await readPullRequest(url);
|
|
415
|
-
const path = !own
|
|
442
|
+
const path = !own?.here || observed.state === 'MERGED' ? null : await originOf(own.path) === repository && await run('git', ['branch', '--show-current'], own.path) === observed.headRefName ? own.path : await branchCheckout(board, id, repository, observed.headRefName);
|
|
416
443
|
// A PR merged before its review (an outage fix landed first) is still reviewed: its head is fixed for good, so the
|
|
417
444
|
// verdict is recorded on that head, and nothing about a worktree or its base can move under it any more.
|
|
418
445
|
const merged = observed.state === 'MERGED';
|
package/board/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { homeOf, parse } from './args.mjs';
|
|
|
11
11
|
import { BOARD_OPERATION_VERBS, boardHome, boardOwnerSocket, CALLER_FACTS } from '@volter/teams/board-owner';
|
|
12
12
|
import { current, isCallersCall, ownCall, within } from './call.mjs';
|
|
13
13
|
import { taskStatusOf } from './published.mjs';
|
|
14
|
+
import { executionOf } from './session-work.mjs';
|
|
14
15
|
import { createServer } from 'node:net';
|
|
15
16
|
import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, copyFileSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
16
17
|
import { notRuntime, ownHome } from './owner-files.mjs';
|
|
@@ -93,7 +94,7 @@ The board's verbs, with their flags:
|
|
|
93
94
|
set <id> <assignee|machine|model|every> <value|none> [--provider] [--ids ...] assign <id> <profile|none> set-model <id> [model] [--provider] set-goal <id>... [--goal-max-turns N] [--off]
|
|
94
95
|
reassign <id> --agent AGENT [--role implementer] (moves a card role's assignment to another agent; sessions keep their identity)
|
|
95
96
|
claim <id> [--ttl] reclaim <id> [--reason] heartbeat <id> [--note] [--until <time|30m>] link|unlink <parent> <child>
|
|
96
|
-
comment <id> (<text> | --file PATH|-) [--author] [--max-len] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
|
|
97
|
+
comment <id> (<text> | --file PATH|-) [--author] [--max-len] [(--pr URL --pr-head SHA)...] attach <id> <path> [--name] [--content-type] attachments <id> attach-rm <n>
|
|
97
98
|
complete <id>... [--result] [--summary] [--metadata JSON] edit <id> --result [--summary] [--metadata]
|
|
98
99
|
block <id> [reason] (--on <card> | --waiting-on <message id>) [--ids ...] schedule <id> [reason] [--ids ...]
|
|
99
100
|
pause <id> --reason <why> resume <id> cancel <id> --reason <why>
|
|
@@ -135,6 +136,7 @@ And:
|
|
|
135
136
|
|
|
136
137
|
specify <id> --workspace dir:<repository> --branch NAME: the manager retargets a stopped card without replacing its history.
|
|
137
138
|
specify <id> --workspace … --machine M: declares the workspace on M and moves the card there, never asking the machine it leaves.
|
|
139
|
+
comment <id> TEXT (--pr URL --pr-head SHA)...: the card's implementer or the manager adds PRs (including merged ones) to the arc's discovered source set, in the card's own repositories, each followed at its current head; the latest declaration replaces the previous one. This records evidence; only the board starts its review.
|
|
138
140
|
request-review <id> (--pr URL --pr-head SHA)...: hand over every open PR the arc changed, each at its pushed head; one review covers them all.
|
|
139
141
|
request-state <id> --state done (--reviewed-sha SHA)...: record the verdict on every requested PR head.
|
|
140
142
|
request-state <id>... --state STATE --reason REASON: request several decisions in one retained manager mail.
|
|
@@ -289,14 +291,14 @@ const ts = (t) => {
|
|
|
289
291
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
290
292
|
};
|
|
291
293
|
// a card's status is the workflow's (triaging, reviewing, …); kanban.db stores the Hermes status it maps to
|
|
292
|
-
const line = (t) => `${ICONS[t.status] ?? '?'} ${t.id} ${(t.stage ?? t.status).padEnd(8)} ${(t.assignee || '(unassigned)').padEnd(20)}${t.tenant ? ` [${t.tenant}]` : ''} ${t.title}${t.machine ? ` @${t.machine}` : ''}`;
|
|
294
|
+
const line = (t) => `${ICONS[t.status] ?? '?'} ${t.id} ${(t.stage ?? t.status).padEnd(8)}${['running','review'].includes(t.status) ? ` [session ${t.execution?.state ?? 'unknown'}]` : ''} ${(t.assignee || '(unassigned)').padEnd(20)}${t.tenant ? ` [${t.tenant}]` : ''} ${t.title}${t.machine ? ` @${t.machine}` : ''}`;
|
|
293
295
|
const cardDict = (t) => ({
|
|
294
296
|
id: t.id, title: t.title, body: t.body, assignee: t.assignee, status: t.status, priority: t.priority, tenant: t.tenant,
|
|
295
297
|
workspace_kind: t.workspace_kind, repository: t.workspace_anchor ?? null, workspace_path: t.workspace_path, branch_name: t.branch_name, project_id: t.project_id,
|
|
296
298
|
created_by: t.created_by, created_at: t.created_at, started_at: t.started_at, completed_at: t.completed_at, result: t.result,
|
|
297
299
|
skills: t.skills ?? [], max_retries: t.max_retries, model_override: t.model_override, provider_override: t.provider_override,
|
|
298
300
|
session_id: t.session_id, workflow_template_id: t.workflow_template_id, current_step_key: t.current_step_key,
|
|
299
|
-
machine: t.machine ?? null, subtask_of: t.subtask_of ?? null,
|
|
301
|
+
machine: t.machine ?? null, subtask_of: t.subtask_of ?? null, execution: t.execution ?? null,
|
|
300
302
|
});
|
|
301
303
|
|
|
302
304
|
/** Hermes's durations: seconds, or 30s / 5m / 2h / 1d. */
|
|
@@ -523,7 +525,7 @@ export async function runVerb(argv) {
|
|
|
523
525
|
return [t.id, {
|
|
524
526
|
workflow_status: t.stage ?? t.status,
|
|
525
527
|
parents: b.db.prepare('SELECT parent_id FROM task_links WHERE child_id = ? ORDER BY parent_id').all(t.id).map((r) => r.parent_id),
|
|
526
|
-
run: run ? { id: run.id, status: run.status, started_at: run.started_at, session: session.address ?? session.session_id ?? null } : null,
|
|
528
|
+
run: run ? { id: run.id, status: run.status, started_at: run.started_at, session: session.address ?? session.session_id ?? null, execution: executionOf(run) } : null,
|
|
527
529
|
// the message whose answer its standing block waits for (`block --waiting-on`)
|
|
528
530
|
waiting_on: (() => {
|
|
529
531
|
const last = b.db.prepare("SELECT kind, payload FROM task_events WHERE task_id = ? AND kind IN ('blocked', 'unblocked', 'dependency_wait') ORDER BY id DESC LIMIT 1").get(t.id);
|
|
@@ -546,6 +548,8 @@ export async function runVerb(argv) {
|
|
|
546
548
|
const card = withBoard((b) => {
|
|
547
549
|
const got = engine.showCard(b.db, id);
|
|
548
550
|
got.runs = engine.runs(b.db, id, { stateType: args['state-type'] ?? null, stateName: args['state-name'] ?? null });
|
|
551
|
+
got.task.execution = executionOf(engine.currentRun(b.db, id));
|
|
552
|
+
got.runs = got.runs.map(run => ({ ...run, execution: executionOf(run) }));
|
|
549
553
|
got.diagnostics = engine.diagnose(b.db, got.task, { failureLimit: settings.failureLimit, launchWindowSeconds: settings.launchWindow });
|
|
550
554
|
got.stage = statusOf(b.db, got.task);
|
|
551
555
|
got.time = scopeOf(b.db, b, got.task, { params: workflowFor(root).params }).card.time;
|
|
@@ -603,7 +607,7 @@ export async function runVerb(argv) {
|
|
|
603
607
|
for (const r of card.runs) {
|
|
604
608
|
const s = r.metadata?.supercode;
|
|
605
609
|
out.push(` #${String(r.id).padEnd(3)} ${(r.outcome || r.status || 'active').padEnd(12)} @${r.profile || '-'} ${r.ended_at ? `${Math.max(0, r.ended_at - r.started_at)}s` : 'active'} ${ts(r.started_at)}`);
|
|
606
|
-
if (s) out.push(` ⇢ ${s.session_id ? `session ${s.session_id}${s.pane ? ` in pane ${s.pane}` : ''}` : `worker pid ${s.pid}`} on ${s.machine} (${s.mode})`);
|
|
610
|
+
if (s) out.push(` ⇢ ${s.session_id ? `session ${s.session_id}${s.pane ? ` in pane ${s.pane}` : ''}` : `worker pid ${s.pid}`} on ${s.machine} (${s.mode}); observed ${executionOf(r).state}`);
|
|
607
611
|
if (r.summary) out.push(` → ${r.summary.split('\n')[0].slice(0, 160)}`);
|
|
608
612
|
if (r.error) out.push(` ! ${r.error.split('\n')[0].slice(0, 160)}`);
|
|
609
613
|
}
|
|
@@ -717,7 +721,7 @@ export async function runVerb(argv) {
|
|
|
717
721
|
need(args.file !== undefined ? 1 : 2, 'a card id and the text (or --file PATH, - for stdin)');
|
|
718
722
|
let text = textFrom(args.file, rest.slice(1).join(' '));
|
|
719
723
|
if (args['max-len']) text = text.slice(0, Number(args['max-len']));
|
|
720
|
-
const n = await withBoardAsync((b) => engine.comment(b, id, isLatestReviewer(b.db, id, actorOf()) ? actorOf() : args.author ?? callerAddress() ?? callerName(), text));
|
|
724
|
+
const n = await withBoardAsync((b) => engine.comment(b, id, isLatestReviewer(b.db, id, actorOf()) ? actorOf() : args.author ?? callerAddress() ?? callerName(), text, { pr: args.pr ?? null, pr_head: args['pr-head'] ?? null }));
|
|
721
725
|
say(args, `Comment added to ${id}`, { ok: true, comment: n }); return 0;
|
|
722
726
|
}
|
|
723
727
|
case 'attach': {
|
|
@@ -966,9 +970,9 @@ export async function runVerb(argv) {
|
|
|
966
970
|
// planned through the workflow's own dispatch (its start event, eligible, order and limits), writing nothing
|
|
967
971
|
const reports = await plan({ root, lanes, settings, params, slugs: [slug] });
|
|
968
972
|
const would = reports.flatMap((r) => r.would), reviews = reports.flatMap((r) => r.would_review ?? []);
|
|
969
|
-
const lines = [...reviews.map((r) => `would request review of ${r.id}: ${r.no_code ? `no code changed (${r.no_code.repository} ${r.no_code.branch} holds no commit beyond ${r.no_code.base}, no PR from it)` : r.prs.map((url, i) => `${url} at ${r.heads[i]}`).join(', ')}`),
|
|
973
|
+
const lines = [...reviews.map((r) => `would request review of ${r.id}: ${r.no_code ? r.no_code.kind === 'disposition' ? 'scratch operational disposition (card evidence and closed tasks)' : `no code changed (${r.no_code.repository} ${r.no_code.branch} holds no commit beyond ${r.no_code.base}, no PR from it)` : r.prs.map((url, i) => `${url} at ${r.heads[i]}`).join(', ')}`),
|
|
970
974
|
...would.map((w) => `would start ${w.id} (${w.status}${w.role ? `, its ${w.role}` : ''}) as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`)];
|
|
971
|
-
say(args, lines.length ? lines.join('\n') : 'nothing would start', args.json ? { would, would_review: reviews } : would);
|
|
975
|
+
say(args, lines.length ? lines.join('\n') : 'nothing would start', args.json ? { would, would_review: reviews, queued: reports.flatMap(r => r.queued ?? []) } : would);
|
|
972
976
|
return 0;
|
|
973
977
|
}
|
|
974
978
|
// This process holds the home's lock for the round, so it is the board's owner for it: it registers its pid, which
|
package/board/dispatch.mjs
CHANGED
|
@@ -340,6 +340,15 @@ const unavailable = message => Object.assign(new Error(message), { launchDeferre
|
|
|
340
340
|
|
|
341
341
|
// ------------------------------------------------------------------ supervise
|
|
342
342
|
|
|
343
|
+
/** Material card work (workflow.mjs sessionProgress) recorded since the run began. */
|
|
344
|
+
function progressedSince(db, task, runRow) {
|
|
345
|
+
const started = Number(runRow?.started_at ?? 0);
|
|
346
|
+
const progress = sessionProgress(db, task);
|
|
347
|
+
if (progress.commit >= started && progress.commit > 0) return true;
|
|
348
|
+
const at = progress.event ? db.prepare('SELECT created_at FROM task_events WHERE id=?').get(progress.event)?.created_at : null;
|
|
349
|
+
return at != null && Number(at) >= started;
|
|
350
|
+
}
|
|
351
|
+
|
|
343
352
|
async function supervise(board, fleet, settings, report, entry, effects) {
|
|
344
353
|
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();
|
|
345
354
|
// Published native observations are the only liveness input. Missing data remains unknown. A launch the
|
|
@@ -352,7 +361,14 @@ async function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
352
361
|
if (!session?.launch_key || !fleet.ok || sessionAlive(fleet, session) !== false) continue;
|
|
353
362
|
// how a keyed launch ended, where its machine keeps that record: read when it can be, never waited on
|
|
354
363
|
if (!fleet.capabilities?.get(normalMachine(session.machine))?.launches) continue;
|
|
355
|
-
try {
|
|
364
|
+
try {
|
|
365
|
+
const receipt = await launchReceipt(board, session);
|
|
366
|
+
if (receipt?.live === false && receipt.exit) {
|
|
367
|
+
exits.set(id, { ...receipt.exit, launch_key: session.launch_key });
|
|
368
|
+
// a clean exit is judged against the card's commits, read before the board write
|
|
369
|
+
if (!receipt.exit.failed && recoversRuns(entry)) await refreshCardInputs(board, id, { commits: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
356
372
|
catch (error) { report.failed.push([id, `exit receipt unavailable; retaining liveness observation: ${error.message}`]); }
|
|
357
373
|
}
|
|
358
374
|
for (const { id } of rows) {
|
|
@@ -371,6 +387,21 @@ async function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
371
387
|
event(db,id,'session_rebound',{session_id:session.session_id,pane:session.pane},runRow.id);
|
|
372
388
|
}
|
|
373
389
|
const t = now();
|
|
390
|
+
if (runRow && session) {
|
|
391
|
+
const alive = session.surface === 'headless' ? pidAlive(session.pid) : sessionAlive(fleet, session);
|
|
392
|
+
let native = fleet.sessions?.find(item => item.address === addressOf(session));
|
|
393
|
+
if (!native && session.session_id) try {
|
|
394
|
+
const row = nativeReading(nativeSessions(session.machine, session.harness ?? 'claude-code', session.session_id), session).row;
|
|
395
|
+
native = { turn: row?.activity?.turn };
|
|
396
|
+
} catch { /* an unread turn stays unknown */ }
|
|
397
|
+
const reading = { alive: alive ?? null, turn: alive === true ? native?.turn ?? null : null, observed_at: t };
|
|
398
|
+
const prior = session.presence_observation;
|
|
399
|
+
// Keep state changes immediately and renew unchanged readings once a minute.
|
|
400
|
+
if (!prior || prior.alive !== reading.alive || prior.turn !== reading.turn || t - prior.observed_at >= 60) {
|
|
401
|
+
setRunSession(db, runRow.id, { presence_observation: reading });
|
|
402
|
+
session = { ...session, presence_observation: reading };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
374
405
|
// what the dispatcher saw becomes a platform event; the card's workflow says what it does
|
|
375
406
|
const observe = (name, payload, bucket) => {
|
|
376
407
|
try {
|
|
@@ -474,12 +505,23 @@ async function supervise(board, fleet, settings, report, entry, effects) {
|
|
|
474
505
|
const ended = { code: exit.failed ? 1 : 0, error: `native session ${session.session_id} ended: ${exit.how}`, session_id: session.session_id, pane: session.pane };
|
|
475
506
|
if (!exit.failed) {
|
|
476
507
|
if (monitorsRuns(entry)) { observe('session_closed', {...ended,exit},'crashed'); return; }
|
|
508
|
+
// Under recover a clean exit continues the card. One with no material card progress since its run began
|
|
509
|
+
// is an unsuccessful continuation: it counts toward the failure limit like any failed run, so a session
|
|
510
|
+
// that always ends cleanly (fresh or resumed) stops at the limit and the workflow asks the manager.
|
|
511
|
+
if (recoversRuns(entry) && !progressedSince(db, task, runRow)) {
|
|
512
|
+
observe('exited', { ...ended, error: `${ended.error} with no card progress since run ${runRow.id} began` }, 'crashed');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
477
515
|
try {
|
|
516
|
+
if (recoversRuns(entry)) retainRecovery(db, id, 'session_closed', { ...ended, exit });
|
|
478
517
|
const r = fire(db, board, entry, id, 'session_closed', { roles: ['platform'], payload: ended });
|
|
479
518
|
staged.push(...r.effects); (report.closed_by_hand ??= []).push(id);
|
|
480
519
|
return;
|
|
481
520
|
} catch (error) { if (!error.noTransition) { report.failed.push([id, `session_closed: ${String(error.message).slice(-200)}`]); return; } }
|
|
482
521
|
}
|
|
522
|
+
if (exit.failed && session.mode === 'resume' && !session.seen_at) {
|
|
523
|
+
setRunSession(db, runRow.id, { resumable: false, resume_failed_before_live: true });
|
|
524
|
+
}
|
|
483
525
|
observe('exited', ended, 'crashed');
|
|
484
526
|
return;
|
|
485
527
|
}
|
|
@@ -1797,7 +1839,8 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
|
|
|
1797
1839
|
const launchKey = `board-${task.id}-r${runId}-resume`;
|
|
1798
1840
|
session = await openOwned(launchKey, prev.session_id, 'resume', ['open', '--new', its.program, ...base.slice(3),
|
|
1799
1841
|
'--key', launchKey, '--input', told, '--', ...its.resume(prev.session_id), ...tail]);
|
|
1800
|
-
} else replaced = { session_id: prev.session_id, why: 'its resume never came up'
|
|
1842
|
+
} else replaced = { session_id: prev.session_id, why: 'its resume never came up',
|
|
1843
|
+
letGo: recoversRuns(workflowFor(board.root)) && Boolean(prev.resume_failed_before_live) && Boolean(prev.close_confirmed_at) };
|
|
1801
1844
|
} else if (prev) replaced = { session_id: prev.session_id, why: `the card moved from ${prev.machine} to ${machine}` };
|
|
1802
1845
|
// a card keeps its context: a slot's session that cannot be kept or resumed stops the launch, and only a session the
|
|
1803
1846
|
// workflow let go (`forget_session`) or its manager did (a reassignment to another harness's lane) is replaced
|
|
@@ -2631,17 +2674,30 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
|
|
|
2631
2674
|
return out;
|
|
2632
2675
|
}
|
|
2633
2676
|
|
|
2634
|
-
/** Whether an arc with no open PR may be one that changed no code and is due its review: it
|
|
2635
|
-
* (a fresh arc with
|
|
2636
|
-
* closing the tasks that own the findings makes it
|
|
2677
|
+
/** Whether an arc with no open PR may be one that changed no code and is due its review: it has done work, by filing
|
|
2678
|
+
* tasks or by a run that ended with an Acceptance outcome ticked (a fresh arc with neither has not started), and no
|
|
2679
|
+
* review of it is pending or passed (after changes are requested, closing the tasks that own the findings makes it
|
|
2680
|
+
* due again). */
|
|
2637
2681
|
function noCodeReviewDue(board, id) {
|
|
2638
2682
|
return view(board, (db) => {
|
|
2639
|
-
|
|
2683
|
+
const filed = db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id);
|
|
2684
|
+
if (!filed && !(db.prepare('SELECT 1 FROM task_runs WHERE task_id = ? AND ended_at IS NOT NULL LIMIT 1').get(id) &&
|
|
2685
|
+
outcomeTicked(db.prepare('SELECT body FROM tasks WHERE id = ?').get(id)?.body))) return false;
|
|
2640
2686
|
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);
|
|
2641
2687
|
return !last || ['changes_requested','review_voided'].includes(last.kind);
|
|
2642
2688
|
});
|
|
2643
2689
|
}
|
|
2644
2690
|
|
|
2691
|
+
/** Whether a card body ticks some outcome under its Acceptance heading. */
|
|
2692
|
+
function outcomeTicked(body) {
|
|
2693
|
+
let acceptance = false;
|
|
2694
|
+
return String(body ?? '').split('\n').some((line) => {
|
|
2695
|
+
const heading = /^#{1,6}\s+(.+?)\s*$/.exec(line);
|
|
2696
|
+
if (heading) acceptance = heading[1].toLowerCase() === 'acceptance';
|
|
2697
|
+
return acceptance && /^\s*[-*] \[[xX]\] \S/.test(line);
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2645
2701
|
/** An opened PR asks for the declared review transition once the arc's tasks are closed. */
|
|
2646
2702
|
// A task written in the card's own Tasks section that did not become a subtask holds its review too.
|
|
2647
2703
|
/** The arc's session still in a turn: its review, and the close of that session the request brings (end_run), wait for
|
|
@@ -2660,7 +2716,7 @@ async function turnInFlight(board, id, fleet) {
|
|
|
2660
2716
|
async function activatePullRequestReviews(board, entry, report, effects, { only = null, dry = false, fleet = null } = {}) {
|
|
2661
2717
|
const candidates = view(board, db => db.prepare(`SELECT t.id FROM tasks t JOIN supercode_cards c ON c.task_id=t.id
|
|
2662
2718
|
WHERE c.subtask_of IS NULL AND c.every IS NULL
|
|
2663
|
-
|
|
2719
|
+
${only ? 'AND t.id=?' : ''}`).all(...(only ? [only] : [])));
|
|
2664
2720
|
for (const { id } of candidates) {
|
|
2665
2721
|
const eligible = db => {
|
|
2666
2722
|
const task = getTask(db, id);
|
|
@@ -2684,7 +2740,7 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
|
|
|
2684
2740
|
// requested the next run is the implementer's, until a new head is pushed.
|
|
2685
2741
|
// An arc that closed its tasks having changed no code gets its one review of the act itself (ADR 0003).
|
|
2686
2742
|
if (!payload && noCodeReviewDue(board, id)) payload = await noCodeChange(board, id);
|
|
2687
|
-
if (!payload) { report.queued.push([id, 'review waits: no open PR on its
|
|
2743
|
+
if (!payload) { report.queued.push([id, 'review waits: no open or unjudged merged PR on its declared branches, and no completed disposition']); continue; }
|
|
2688
2744
|
if (payload.waits) { report.queued.push([id, `review waits: ${payload.waits}`]); payload = null; continue; }
|
|
2689
2745
|
if (!payload.no_code && !await reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
|
|
2690
2746
|
report.queued.push([id, `review waits: every open PR head (${payload.pr.join(', ')}) has a verdict or predates the last one; push a commit that answers it`]);
|
|
@@ -2700,8 +2756,8 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
|
|
|
2700
2756
|
if (eligible(db) !== true) return null;
|
|
2701
2757
|
return withCompletionEvidence(board, id, evidence, () => fire(db, board, entry, id, 'request_review', {
|
|
2702
2758
|
roles: ['platform'], payload: { ...payload, run_id: task.current_run_id, ci: evidence, summary: payload.no_code
|
|
2703
|
-
? `No code changed: ${payload.no_code.repository} ${payload.branch} holds no commit beyond ${payload.no_code.base} and no PR was opened from it
|
|
2704
|
-
: `
|
|
2759
|
+
? `${payload.no_code.kind === 'disposition' ? 'Scratch operational disposition.' : `No code changed: ${payload.no_code.repository} ${payload.branch} holds no commit beyond ${payload.no_code.base} and no PR was opened from it.`} Review the arc's act from the card: its comments, its tasks' results and what they point at.`
|
|
2760
|
+
: `Source ${payload.pr.map((url, index) => `${url} at ${payload.pr_head[index]}`).join(', ')}` }
|
|
2705
2761
|
}));
|
|
2706
2762
|
});
|
|
2707
2763
|
if (moved) {
|
package/board/engine.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Each function takes an open board (`store.mjs`) and runs in one write transaction. Nothing here launches,
|
|
9
9
|
// observes or sends: the dispatcher (`dispatch.mjs`) does, and records what it did through these.
|
|
10
10
|
import { current } from './call.mjs';
|
|
11
|
+
import { executionOf } from './session-work.mjs';
|
|
11
12
|
import { spawn, execFile } from 'node:child_process';
|
|
12
13
|
import { existsSync, rmSync } from 'node:fs';
|
|
13
14
|
import { join, resolve } from 'node:path';
|
|
@@ -721,10 +722,22 @@ export async function unlink(board, parent, child, { roles } = {}) {
|
|
|
721
722
|
}
|
|
722
723
|
|
|
723
724
|
/** A comment on the card's thread; answers its id. */
|
|
724
|
-
export async function comment(board, id, author, body, { roles } = {}) {
|
|
725
|
+
export async function comment(board, id, author, body, { roles, pr = null, pr_head = null } = {}) {
|
|
725
726
|
if (!String(body ?? '').trim()) throw new Error('a comment has a body');
|
|
726
727
|
if (!String(author ?? '').trim()) throw new Error('a comment has an author');
|
|
727
|
-
|
|
728
|
+
let sources;
|
|
729
|
+
if (pr != null || pr_head != null) {
|
|
730
|
+
const urls = [].concat(pr ?? []), heads = [].concat(pr_head ?? []);
|
|
731
|
+
if (!urls.length || urls.length !== heads.length) throw new Error('Record every PR with --pr URL --pr-head SHA, one full head per URL.');
|
|
732
|
+
if (new Set(urls).size !== urls.length) throw new Error('Record each PR once.');
|
|
733
|
+
sources = urls.map((url, i) => {
|
|
734
|
+
if (!/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*\/?$/.test(url) || !/^[a-f0-9]{40}$/.test(heads[i])) throw new Error('A source declaration needs a GitHub PR URL and its full head SHA.');
|
|
735
|
+
return { url: url.replace(/\/$/, ''), head: heads[i] };
|
|
736
|
+
});
|
|
737
|
+
// Only the card's own implementer or the board's manager declares its sources; the author text is not identity.
|
|
738
|
+
if (!(roles ?? callerRoles(board.db, id)).some((role) => role === 'implementer' || role === 'manager')) throw new Error(`Only ${id}'s implementer or the manager records its PR sources.`);
|
|
739
|
+
}
|
|
740
|
+
const result = await act(board, id, 'comment', { roles, payload: { author: author.trim(), body, ...(sources ? { sources } : {}) } });
|
|
728
741
|
return result.commentId;
|
|
729
742
|
}
|
|
730
743
|
|
|
@@ -812,7 +825,7 @@ export function listCards(db, { status = null, assignee = null, machine = null,
|
|
|
812
825
|
const rows = all(db, `SELECT t.*, c.machine, c.subtask_of, c.stage, c.tags, c.sponsor FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id
|
|
813
826
|
WHERE (? IS NULL OR t.status = ? OR c.stage = ?) AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND (? OR t.status != 'archived')
|
|
814
827
|
ORDER BY t.priority DESC, t.created_at ASC`, status, status, status, assignee, assignee, machine, machine, includeArchived ? 1 : 0);
|
|
815
|
-
return rows.map((r) => ({ ...r, skills: json(r.skills, []) ?? [] }));
|
|
828
|
+
return rows.map((r) => ({ ...r, skills: json(r.skills, []) ?? [], execution: executionOf(r.current_run_id ? one(db, 'SELECT metadata,ended_at FROM task_runs WHERE id=?', r.current_run_id) : null) }));
|
|
816
829
|
}
|
|
817
830
|
|
|
818
831
|
export function showCard(db, id) {
|
package/board/incidents.mjs
CHANGED
|
@@ -62,6 +62,33 @@ export function surfaceIncident(db, board, entry, id, name, evidence = {}, runId
|
|
|
62
62
|
/** Older ended runs are audited before eligibility: a completed run is not permission to repeat. */
|
|
63
63
|
export function auditEndedRuns(db, board, entry) {
|
|
64
64
|
if (recoversRuns(entry)) {
|
|
65
|
+
// Retained positive exit receipts from an earlier version qualify the same
|
|
66
|
+
// failed-before-live fallback as a newly supervised resume. An unknown
|
|
67
|
+
// reading, a mere ended row or a once-live resume does not qualify.
|
|
68
|
+
for (const row of db.prepare(`SELECT r.id,r.metadata FROM task_runs r JOIN tasks t ON t.id=r.task_id
|
|
69
|
+
WHERE r.id=(SELECT MAX(id) FROM task_runs WHERE task_id=t.id) AND r.ended_at IS NOT NULL
|
|
70
|
+
AND t.current_run_id IS NULL AND t.status NOT IN ('done','cancelled','archived')`).all()) {
|
|
71
|
+
const metadata = json(row.metadata, {}), session = metadata.supercode;
|
|
72
|
+
if (session?.mode !== 'resume' || session.seen_at || session.resume_failed_before_live) continue;
|
|
73
|
+
const exit = db.prepare("SELECT 1 FROM task_events WHERE run_id=? AND kind='run_incident' AND json_extract(payload,'$.observation')='exited' AND json_extract(payload,'$.evidence.code')=1").get(row.id);
|
|
74
|
+
if (!exit) continue;
|
|
75
|
+
metadata.supercode = { ...session, resumable: false, resume_failed_before_live: true };
|
|
76
|
+
db.prepare('UPDATE task_runs SET metadata=? WHERE id=?').run(JSON.stringify(metadata), row.id);
|
|
77
|
+
}
|
|
78
|
+
// The older organization workflow automatically paused a clean native exit.
|
|
79
|
+
// Release only that exact platform outcome, never a person's later pause.
|
|
80
|
+
for (const task of db.prepare(`SELECT t.id,c.paused FROM tasks t JOIN supercode_cards c ON c.task_id=t.id
|
|
81
|
+
WHERE t.current_run_id IS NULL AND t.status NOT IN ('done','cancelled','archived') AND c.paused IS NOT NULL AND NOT COALESCE(c.archived,0)`).all()) {
|
|
82
|
+
const pause = json(task.paused, null);
|
|
83
|
+
const last = db.prepare("SELECT kind,payload FROM task_events WHERE task_id=? AND kind IN ('closed_by_hand','paused','resumed') ORDER BY id DESC LIMIT 1").get(task.id);
|
|
84
|
+
const ended = json(last?.payload, {});
|
|
85
|
+
// The machine may have moved since this run. Match the retained board
|
|
86
|
+
// actor's exact label, never a user's actor or just the reason text.
|
|
87
|
+
if (last?.kind !== 'closed_by_hand' || !pause?.by?.endsWith(`:board:${board.label}`) || !pause.by.startsWith('sc:')) continue;
|
|
88
|
+
if (pause.reason !== `its session ended cleanly (${ended.error}); resume the card to start it again`) continue;
|
|
89
|
+
db.prepare('UPDATE supercode_cards SET paused=NULL WHERE task_id=? AND paused=?').run(task.id, task.paused);
|
|
90
|
+
event(db, task.id, 'automatic_close_pause_released', { reason: 'workflow selected native continuation after clean exit', prior_pause: pause });
|
|
91
|
+
}
|
|
65
92
|
// Changing the explicit policy releases only incident holds. A person's
|
|
66
93
|
// pause, dependency, dispatch switch or unrelated hold remains their act.
|
|
67
94
|
for (const task of db.prepare(`SELECT t.*,r.id run_id,r.metadata run_metadata FROM tasks t JOIN task_runs r
|
package/board/published.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// supercode_publication_dirty, whoever writes (this process, Hermes's native writer, a document import). A transaction
|
|
6
6
|
// publishes the marked cards and clears their marks; one that marked nothing reads one row and publishes nothing.
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { executionOf } from './session-work.mjs';
|
|
8
9
|
export const PUBLICATION_SCHEMA = `
|
|
9
10
|
CREATE TABLE IF NOT EXISTS supercode_publications (
|
|
10
11
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, resource_id TEXT NOT NULL,
|
|
@@ -95,14 +96,16 @@ export function cardSnapshot(db,row,rows=perCard(db)) {
|
|
|
95
96
|
const comments=rows.comments(row.id).map(comment=>({...comment,created_at:at(comment.created_at)}));
|
|
96
97
|
const verdicts={review_requested:'requested',approved:'approved',review_approved:'approved',changes_requested:'changes_requested',escalated:'escalated'};
|
|
97
98
|
const reviews=rows.reviews(row.id).map(event=>{const payload=parse(event.payload,{});return {id:event.id,verdict:verdicts[event.kind],by:payload.reviewer??payload.by??payload.profile,reason:payload.reason,at:at(event.created_at)};});
|
|
98
|
-
const
|
|
99
|
+
const runs=rows.attempts(row.id);
|
|
100
|
+
const attempts=runs.map(run=>{
|
|
99
101
|
const native=parse(run.metadata,{})?.supercode??null;
|
|
100
102
|
const session=native?{machine:native.machine,harness:native.harness,session_id:native.session_id,address:native.address,pane:native.pane,surface:native.surface,state:native.state,closed_at:native.closed_at,agent:native.agent_id??null,counted:native.counted??null,profile_differences:native.agent_profile_mismatch?.differences??null}:null;
|
|
101
103
|
const metadata=parse(run.metadata,{});delete metadata.supercode;
|
|
102
|
-
return {id:String(run.id),profile:run.profile,step:run.step_key,status:run.status,started_at:at(run.started_at),ended_at:at(run.ended_at),outcome:run.outcome,error:run.error,session,handoff:run.summary||Object.keys(metadata).length?{summary:run.summary,metadata}:null};
|
|
104
|
+
return {id:String(run.id),profile:run.profile,step:run.step_key,status:run.status,started_at:at(run.started_at),ended_at:at(run.ended_at),outcome:run.outcome,error:run.error,execution:executionOf(run),session,handoff:run.summary||Object.keys(metadata).length?{summary:run.summary,metadata}:null};
|
|
103
105
|
});
|
|
104
106
|
const lane=own.stage??row.status;
|
|
105
107
|
return {id:row.id,title:row.title,body:row.body??'',assignee:row.assignee,status:taskStatusOf(lane,row.completed_at),lane,
|
|
108
|
+
execution:executionOf(runs.find(run=>run.id===row.current_run_id)),
|
|
106
109
|
priority:row.priority,created_by:row.created_by,created_at:at(row.created_at),started_at:at(row.started_at),completed_at:at(row.completed_at),result:row.result,skills:parse(row.skills,[]),model:row.model_override,provider:row.provider_override,
|
|
107
110
|
tags:parse(own.tags,null)??(row.tenant?[row.tenant]:[]),sponsor:own.sponsor??null,sponsor_evidence:parse(own.sponsor_evidence),requested_by_unverified:parse(own.requested_by_unverified),machine:own.machine??null,subtask_of:own.subtask_of??null,archived:!!own.archived,
|
|
108
111
|
blocked_by:parse(own.blocked_by,[]),paused:parse(own.paused),every:own.every??null,
|
package/board/session-work.mjs
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
// The dispatcher observes native turns and explicit session delegation. A
|
|
2
2
|
// transcript write, a reminder reply, or sharing a profile proves no progress.
|
|
3
3
|
|
|
4
|
+
/** A retained reading, separate from the card's workflow lane and launch receipt.
|
|
5
|
+
* An ended attempt is never a live session; an unread live attempt is unknown. */
|
|
6
|
+
export function executionOf(run) {
|
|
7
|
+
if (!run) return { state: 'awaiting_session', alive: false, observed_at: null };
|
|
8
|
+
let metadata = run.metadata;
|
|
9
|
+
if (typeof metadata === 'string') { try { metadata = JSON.parse(metadata); } catch { metadata = null; } }
|
|
10
|
+
const session = metadata?.supercode;
|
|
11
|
+
if (run.ended_at != null || session?.close_confirmed_at || session?.released) return { state: 'ended', alive: false, observed_at: session?.presence_observation?.observed_at ?? null };
|
|
12
|
+
const reading = session?.presence_observation;
|
|
13
|
+
if (!reading) return { state: 'unknown', alive: null, observed_at: null };
|
|
14
|
+
const freshUntil = Number(reading.observed_at) + 90;
|
|
15
|
+
// A stopped dispatcher cannot keep advertising its last positive reading.
|
|
16
|
+
// Cached snapshot consumers use the same expiry without another publication.
|
|
17
|
+
if (Date.now() / 1000 > freshUntil) return { state: 'unknown', alive: null, observed_at: reading.observed_at, fresh_until: freshUntil };
|
|
18
|
+
const delegated = session?.idle_observation?.decision === 'delegated_work' && session.idle_observation.turn === reading.turn;
|
|
19
|
+
return { ...reading, fresh_until: freshUntil, state: reading.alive === false ? 'ended' : reading.alive == null ? 'unknown'
|
|
20
|
+
: delegated ? 'delegating' : reading.turn ?? 'live' };
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/** Sessions this session started, transitively. Cycles never imply work. */
|
|
5
24
|
export function delegatedWork(address, edges, sessions) {
|
|
6
25
|
const reached = new Set([address]), pending = [address], working = [];
|
package/board/workflow.mjs
CHANGED
|
@@ -760,7 +760,7 @@ const FORMAT = {
|
|
|
760
760
|
model_override_set: ({ e }) => ({ model: (e.field === 'model' ? e.value : e.model) || null, provider: e.provider || null }),
|
|
761
761
|
goal_set: ({ after }) => ({ goal_mode: Boolean(after.goal_mode), goal_max_turns: after.goal_mode ? after.goal_max_turns ?? null : null }),
|
|
762
762
|
heartbeat: ({ e }) => (e.note || e.until ? { ...(e.note ? { note: e.note } : {}), ...(e.until ? { until: Number(e.until) } : {}) } : null),
|
|
763
|
-
commented: ({ e }) => ({ author: String(e.author ?? 'user').trim(), len: String(e.body ?? '').length }),
|
|
763
|
+
commented: ({ e }) => ({ author: String(e.author ?? 'user').trim(), len: String(e.body ?? '').length, ...(e.sources ? { sources: e.sources } : {}) }),
|
|
764
764
|
archived: () => null,
|
|
765
765
|
status: ({ e, before, to }) => ({ status: to, reason: e.reason ?? 'reopened', previous_status: before.stored_as }),
|
|
766
766
|
machine_set: ({ e, before }) => ({ machine: e.field === 'machine' ? e.value ?? null : e.machine ?? null, was: before.machine }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.114",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|