@volter/supercode-orchestrator 0.4.110 → 0.4.111
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/dispatch.mjs +34 -11
- package/board/ztrack.mjs +46 -18
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
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 { createRequire } from 'node:module';
|
|
18
19
|
import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
19
20
|
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
21
|
import { hostname, tmpdir } from 'node:os';
|
|
@@ -54,10 +55,22 @@ export function localMachine() {
|
|
|
54
55
|
/** The supercode command the dispatcher drives. */
|
|
55
56
|
export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
|
|
56
57
|
|
|
58
|
+
// Installed board workers use their sibling Teams package, not a CLI build's retained checkout.
|
|
59
|
+
// An explicit operator override remains authoritative for source development.
|
|
60
|
+
let teamsEntry;
|
|
61
|
+
function commandEnv(as = null) {
|
|
62
|
+
if (teamsEntry === undefined) {
|
|
63
|
+
try { teamsEntry = createRequire(import.meta.url).resolve('@volter/supercode-teams/bin'); }
|
|
64
|
+
catch { teamsEntry = null; }
|
|
65
|
+
}
|
|
66
|
+
return { ...process.env, ...(!process.env.SUPERCODE_TEAMS_ENTRY && teamsEntry ? { SUPERCODE_TEAMS_ENTRY: teamsEntry } : {}),
|
|
67
|
+
...(as ? { SUPERCODE_CALLER: as } : {}) };
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
|
|
58
71
|
// a neutral directory: `supercode teams …` resolves its entry from a checkout at the cwd. `as`: the board this
|
|
59
72
|
// command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
|
|
60
|
-
const env = as
|
|
73
|
+
const env = commandEnv(as);
|
|
61
74
|
const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
|
|
62
75
|
return { code: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? r.error.message : '') };
|
|
63
76
|
}
|
|
@@ -166,6 +179,7 @@ export function boardSettings(params = {}) {
|
|
|
166
179
|
ttl: claimTtl(params.claim_ttl_seconds),
|
|
167
180
|
grace: pos(params.session_grace_seconds) ?? 120,
|
|
168
181
|
interval: Math.max(Number(params.dispatch_interval_seconds) || 60, 1),
|
|
182
|
+
idleQuiet: pos(params.session_idle_quiet_seconds) ?? 600,
|
|
169
183
|
logRotateBytes: pos(params.worker_log_rotate_bytes) ?? 2 * 1024 * 1024,
|
|
170
184
|
logBackups: Number.isInteger(Number(params.worker_log_backup_count)) && Number(params.worker_log_backup_count) >= 0 ? Number(params.worker_log_backup_count) : 1,
|
|
171
185
|
hooks: params.hooks && typeof params.hooks === 'object' ? params.hooks : {},
|
|
@@ -525,10 +539,11 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
|
|
|
525
539
|
for (const row of rows) {
|
|
526
540
|
let session = json(board.db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
|
|
527
541
|
if (session?.launch_key && !session.pane && (row.ended_at || session.close_requested)) {
|
|
528
|
-
|
|
542
|
+
let receipt = null, receiptError = null;
|
|
543
|
+
try { receipt = launchReceipt(board, session); } catch (error) { receiptError = error.message; }
|
|
529
544
|
if (receipt?.pane) { bindLaunch(board, row, session, receipt.pane, session.session_id); session = { ...session, pane: receipt.pane }; }
|
|
530
545
|
else {
|
|
531
|
-
const error = `close deferred: awaiting launch receipt ${session.launch_key}`;
|
|
546
|
+
const error = `close deferred: ${receiptError ?? `awaiting launch receipt ${session.launch_key}`}`;
|
|
532
547
|
tx(board, (db) => {
|
|
533
548
|
setRunSession(db, row.id, { close_error: error, close_requested: session.close_requested ?? now() });
|
|
534
549
|
if (session.close_error !== error) event(db, row.task_id, 'session_close_failed', { error, retry: true }, row.id);
|
|
@@ -596,9 +611,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
596
611
|
// a supercode that reports no last message time gives no reading, and no reading gives no verdict
|
|
597
612
|
const lastAt = Math.floor(Number(live?.last_message_at_ms) / 1000);
|
|
598
613
|
if (!(lastAt > 0)) return;
|
|
599
|
-
if (now() - lastAt < settings.
|
|
600
|
-
if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.
|
|
601
|
-
const worked = Boolean(session.goal_nudged_at) && lastAt
|
|
614
|
+
if (now() - lastAt < settings.idleQuiet) return;
|
|
615
|
+
if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.idleQuiet) return;
|
|
616
|
+
const worked = Boolean(session.goal_nudged_at) && lastAt > session.goal_nudged_at;
|
|
602
617
|
try {
|
|
603
618
|
const where = whereOf(board, task);
|
|
604
619
|
// the ask names what the session may do, as its run's prompt did
|
|
@@ -732,9 +747,13 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
|
|
|
732
747
|
|
|
733
748
|
/** A transport error is not a negative launch receipt. The key remains owned by its run. */
|
|
734
749
|
function launchReceipt(board, session) {
|
|
735
|
-
const r = sc(['teams', '
|
|
750
|
+
const r = sc(['teams', 'launches', 'get', session.launch_key, '--machine', session.machine],
|
|
736
751
|
{ timeout: 30_000, as: boardAddress(board.label) });
|
|
737
|
-
|
|
752
|
+
if (r.code !== 0) throw new Error(`launch receipt unavailable on ${session.machine}: install the matching Teams CLI and machine capability (teams launches get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
|
|
753
|
+
if (r.stdout.trim() === 'null') return null;
|
|
754
|
+
const receipt = lastJson(r.stdout);
|
|
755
|
+
if (!receipt || receipt.key !== session.launch_key) throw new Error(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
|
|
756
|
+
return receipt;
|
|
738
757
|
}
|
|
739
758
|
|
|
740
759
|
function bindLaunch(board, row, session, pane, sid) {
|
|
@@ -758,6 +777,8 @@ function bindLaunch(board, row, session, pane, sid) {
|
|
|
758
777
|
|
|
759
778
|
/** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
|
|
760
779
|
export function reconcileCallerLaunch(board, sid) {
|
|
780
|
+
if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
|
|
781
|
+
WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
|
|
761
782
|
const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
|
|
762
783
|
JOIN tasks t ON t.current_run_id=r.id WHERE r.ended_at IS NULL
|
|
763
784
|
AND json_extract(r.metadata, '$.supercode.launch_pending') = 1`).all());
|
|
@@ -846,10 +867,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
846
867
|
if (prev?.close_requested && !prev.closed_at) throw new Error(`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'}`);
|
|
847
868
|
}
|
|
848
869
|
const openOwned = (key, sid, mode, args) => {
|
|
870
|
+
launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
|
|
849
871
|
tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
|
|
850
872
|
session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
|
|
851
873
|
const result = sc(args, { timeout: 180_000, as: boardAddress(board.label) });
|
|
852
|
-
|
|
874
|
+
let facts = null, receiptError = null;
|
|
875
|
+
try { facts = launchReceipt(board, { machine, launch_key: key }); } catch (error) { receiptError = error.message; }
|
|
853
876
|
const pane = facts?.pane ?? lastJson(result.stdout)?.pane ?? null;
|
|
854
877
|
// The daemon's keyed receipt is authoritative even when the stream carrying open's reply closed.
|
|
855
878
|
if (pane) tx(board, (db) => {
|
|
@@ -863,7 +886,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
863
886
|
launch_pending: false, launch_error: null, launched_at: now() };
|
|
864
887
|
}
|
|
865
888
|
const reason = facts?.live === false && pane ? `launch pane ${pane} ended` :
|
|
866
|
-
`launch ${key} is awaiting its machine receipt or session identity: ${(result.stderr || result.stdout).trim().slice(-300)}`;
|
|
889
|
+
`launch ${key} is awaiting its machine receipt or session identity: ${receiptError ?? (result.stderr || result.stdout).trim().slice(-300)}`;
|
|
867
890
|
tx(board, (db) => {
|
|
868
891
|
setRunSession(db, runId, { launch_pending: true, launch_error: reason });
|
|
869
892
|
event(db, task.id, 'session_launch_pending', { key, pane, reason }, runId);
|
|
@@ -1423,7 +1446,7 @@ export function boardLabel(root, slug) {
|
|
|
1423
1446
|
/** `sc` without blocking the tick: notices to many sessions go out side by side. */
|
|
1424
1447
|
function scAsync(args, { input = null, timeout = 120_000 } = {}) {
|
|
1425
1448
|
return new Promise((done) => {
|
|
1426
|
-
const child = spawnChild(supercodeBin(), args, { cwd: tmpdir(), env:
|
|
1449
|
+
const child = spawnChild(supercodeBin(), args, { cwd: tmpdir(), env: commandEnv(), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1427
1450
|
let stdout = '';
|
|
1428
1451
|
let stderr = '';
|
|
1429
1452
|
const timer = setTimeout(() => { stderr += ` timed out after ${timeout}ms`; child.kill('SIGKILL'); }, timeout);
|
package/board/ztrack.mjs
CHANGED
|
@@ -136,6 +136,7 @@ class ZtrackBoard {
|
|
|
136
136
|
if(source.name==='supercode_cards')return JSON.stringify(canonical(source.row,new Set(['since','after_fired','next_run_at'])))!==JSON.stringify(canonical(this.rows.cachedCards[JSON.parse(source.key)[0]],new Set(['since','after_fired','next_run_at'])));
|
|
137
137
|
return false;
|
|
138
138
|
});
|
|
139
|
+
this.publishedFingerprints = {};
|
|
139
140
|
const documents = this.seed || documentChanges.length ? this.documents(this.seed ? null : documentChanges) : {open:this.text, archived:this.archiveText};
|
|
140
141
|
const files = [{ path: this.path, before: hash(this.text), base: this.text, text: documents.open }, { path: this.archive, before: hash(this.archiveText), base: this.archiveText, text: documents.archived }];
|
|
141
142
|
const snapshot = this.snapshot(documents);
|
|
@@ -330,18 +331,7 @@ class ZtrackBoard {
|
|
|
330
331
|
if (this.originals.has(id)) throw new Error(`duplicate board card ${id}`);
|
|
331
332
|
this.originals.set(id, card); this.fileIds.set(id, card.id);
|
|
332
333
|
this.loadCard(id, card);
|
|
333
|
-
for (const
|
|
334
|
-
const tid = task.metadata?.id ?? this.saved.taskIds?.[`${id}:${task.id}`] ?? `t_${randomBytes(4).toString('hex')}`;
|
|
335
|
-
this.taskKeys.set(tid, `${id}:${task.id}`);
|
|
336
|
-
const body = task.metadata?.body ?? '';
|
|
337
|
-
const taskCard = { ...task.metadata?.card, id: tid, title: `${task.id} ${task.text}`, body,
|
|
338
|
-
status: task.status === 'passed' ? (task.metadata?.status === 'cancelled' ? 'cancelled' : 'done') : (['done','cancelled'].includes(task.metadata?.status) ? 'todo' : task.metadata?.status ?? 'todo'),
|
|
339
|
-
metadata: task.metadata?.properties ?? {}, assignee: task.metadata?.assignee ?? card.assignee,
|
|
340
|
-
machine: task.metadata?.machine ?? card.machine, acceptanceCriteria: [],
|
|
341
|
-
acceptance: task.lines ?? [], relations: (task.blockedBy ?? []).map((b) => ({ type: 'blocked-by', issueId: b.ac ? `${nativeId(b.issue)}:${b.ac}` : nativeId(b.issue) })) };
|
|
342
|
-
if (task.waitingOn) taskCard.relations.push({ type: 'blocked-by', issueId: task.waitingOn });
|
|
343
|
-
this.loadCard(tid, taskCard, id);
|
|
344
|
-
}
|
|
334
|
+
for (const [tid, taskCard] of this.taskCards(id, card)) this.loadCard(tid, taskCard, id);
|
|
345
335
|
}
|
|
346
336
|
// Keep the last readable projection only for the affected malformed cards.
|
|
347
337
|
// Their raw document spans remain untouched and their verbs refuse with this reason.
|
|
@@ -368,24 +358,48 @@ class ZtrackBoard {
|
|
|
368
358
|
}
|
|
369
359
|
if (observe) this.documentEvents();
|
|
370
360
|
}
|
|
361
|
+
taskCards(id, card) {
|
|
362
|
+
return card.acceptanceCriteria.map((task) => {
|
|
363
|
+
const tid = task.metadata?.id ?? this.saved.taskIds?.[`${id}:${task.id}`] ?? `t_${randomBytes(4).toString('hex')}`;
|
|
364
|
+
this.taskKeys.set(tid, `${id}:${task.id}`);
|
|
365
|
+
const taskCard = { ...task.metadata?.card, id: tid, title: `${task.id} ${task.text}`, body: task.metadata?.body ?? '',
|
|
366
|
+
status: task.status === 'passed' ? (task.metadata?.status === 'cancelled' ? 'cancelled' : 'done') : (['done','cancelled'].includes(task.metadata?.status) ? 'todo' : task.metadata?.status ?? 'todo'),
|
|
367
|
+
metadata: task.metadata?.properties ?? {}, assignee: task.metadata?.assignee ?? card.assignee,
|
|
368
|
+
machine: task.metadata?.machine ?? card.machine, acceptanceCriteria: [], acceptance: task.lines ?? [],
|
|
369
|
+
relations: (task.blockedBy ?? []).map((b) => ({ type: 'blocked-by', issueId: b.ac ? `${nativeId(b.issue)}:${b.ac}` : nativeId(b.issue) })) };
|
|
370
|
+
if (task.waitingOn) taskCard.relations.push({ type: 'blocked-by', issueId: task.waitingOn });
|
|
371
|
+
return [tid, taskCard];
|
|
372
|
+
});
|
|
373
|
+
}
|
|
371
374
|
loadCard(id, card, parent = null) {
|
|
375
|
+
const rows = this.cardRows(id, card, parent);
|
|
376
|
+
this.insert('tasks', rows.task); this.insert('supercode_cards', rows.state);
|
|
377
|
+
}
|
|
378
|
+
cardRows(id, card, parent = null) {
|
|
372
379
|
const meta = card.metadata ?? {};
|
|
373
380
|
const stored = this.preset.serializeKanbanCard({ ...card, acceptanceCriteria: [], relations: [], metadata: undefined, paused: undefined, every: undefined, workspace: undefined, branch: undefined, machine: undefined, priority: undefined });
|
|
374
381
|
const workspace = /^(scratch|dir|worktree)(?::(.*))?$/.exec(card.workspace ?? 'scratch');
|
|
375
|
-
|
|
382
|
+
const task = { ...meta, ...this.saved.runtime?.[id], id, title: card.title, body: stored.body,
|
|
376
383
|
status: card.status, assignee: card.assignee ?? null, created_at: meta.created_at ?? now(), priority: card.priority ?? 0,
|
|
377
|
-
workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[1] === 'scratch' ? (this.saved.runtime?.[id]?.workspace_path ?? meta.workspace_path ?? null) : workspace?.[2] ?? null, branch_name: card.branch ?? null }
|
|
378
|
-
|
|
384
|
+
workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[1] === 'scratch' ? (this.saved.runtime?.[id]?.workspace_path ?? meta.workspace_path ?? null) : workspace?.[2] ?? null, branch_name: card.branch ?? null };
|
|
385
|
+
const state = { ...this.saved.clocks?.[id], task_id: id, stage: card.status, machine: card.machine ?? null, subtask_of: parent,
|
|
379
386
|
blocked_by: JSON.stringify((card.relations ?? []).map((r) => ({ ...(meta.blocked_by ?? []).find((b) => b.id === nativeId(r.issueId)), id: nativeId(r.issueId), kind: /^[ma]-/.test(r.issueId) ? 'message' : 'card' }))),
|
|
380
|
-
paused: card.paused ? JSON.stringify(card.paused) : null, archived: meta.archived ? 1 : 0, every: card.every ?? null }
|
|
387
|
+
paused: card.paused ? JSON.stringify(card.paused) : null, archived: meta.archived ? 1 : 0, every: card.every ?? null };
|
|
388
|
+
return { task, state };
|
|
389
|
+
}
|
|
390
|
+
fingerprint(t, c) {
|
|
391
|
+
const status = c.stage ?? t.status;
|
|
392
|
+
return { hash: hash(JSON.stringify([t.title, t.body, status, t.assignee, c.machine, c.blocked_by, c.paused, c.every])), title: hash(t.title), body: hash(t.body ?? ''), status, assignee: t.assignee, machine: c.machine, parent: c.subtask_of };
|
|
381
393
|
}
|
|
382
394
|
fingerprints() {
|
|
383
395
|
return Object.fromEntries(this.mem.prepare('SELECT * FROM tasks').all().map((t) => {
|
|
384
396
|
const c = this.mem.prepare('SELECT * FROM supercode_cards WHERE task_id = ?').get(t.id) ?? {};
|
|
385
|
-
return [t.id,
|
|
397
|
+
return [t.id, this.fingerprint(t, c)];
|
|
386
398
|
}));
|
|
387
399
|
}
|
|
388
400
|
documentEvents() {
|
|
401
|
+
// A published native document is already represented by its authored events.
|
|
402
|
+
if (this.saved.documentHashes?.open === hash(this.text) && this.saved.documentHashes?.archived === hash(this.archiveText)) return;
|
|
389
403
|
const previous = this.saved.fingerprints ?? {};
|
|
390
404
|
const current = this.fingerprints();
|
|
391
405
|
if (!Object.keys(previous).length) return;
|
|
@@ -412,6 +426,7 @@ class ZtrackBoard {
|
|
|
412
426
|
return 'document-editor';
|
|
413
427
|
}
|
|
414
428
|
documents(changes=null) {
|
|
429
|
+
this.publishedFingerprints = {};
|
|
415
430
|
const allRows = this.mem.prepare('SELECT * FROM tasks ORDER BY created_at, id').all();
|
|
416
431
|
const states = new Map(this.mem.prepare('SELECT * FROM supercode_cards').all().map((c) => [c.task_id, c]));
|
|
417
432
|
const affected=changes?new Set(changes.map(({source})=>{const id=JSON.parse(source.key)[0];return states.get(id)?.subtask_of??this.rows.cachedCards[id]?.subtask_of??id;})):null;
|
|
@@ -456,6 +471,15 @@ class ZtrackBoard {
|
|
|
456
471
|
for (const [id, card] of cards) {
|
|
457
472
|
const serialized = this.preset.serializeKanbanCard(card);
|
|
458
473
|
const record = { id: card.id, title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
|
|
474
|
+
// Fingerprint the codec's persisted projection, not the pre-serialization input.
|
|
475
|
+
// The codec normalizes bodies (including their trailing newline) and subtask titles.
|
|
476
|
+
const persisted = this.preset.KanbanRootSchema.parse(this.preset.parseKanban([record])).issues[0];
|
|
477
|
+
const rows = this.cardRows(id, persisted);
|
|
478
|
+
this.publishedFingerprints[id] = this.fingerprint(rows.task, rows.state);
|
|
479
|
+
for (const [tid, taskCard] of this.taskCards(id, persisted)) {
|
|
480
|
+
const rows = this.cardRows(tid, taskCard, id);
|
|
481
|
+
this.publishedFingerprints[tid] = this.fingerprint(rows.task, rows.state);
|
|
482
|
+
}
|
|
459
483
|
(states.get(id)?.archived || ['done', 'cancelled'].includes(card.status) ? archived : open).push(record);
|
|
460
484
|
}
|
|
461
485
|
const original=this.documentRecords;
|
|
@@ -463,7 +487,11 @@ class ZtrackBoard {
|
|
|
463
487
|
return { open: !changedPath(this.path)?this.text:this.codec.renderBoardDocument(this.text, open, this.path, [...(this.preserve.get(this.path)??[])]), archived: !changedPath(this.archive)?this.archiveText:this.codec.renderBoardDocument(this.archiveText, archived, this.archive, [...(this.preserve.get(this.archive)??[])]) };
|
|
464
488
|
}
|
|
465
489
|
snapshot(documents) {
|
|
466
|
-
|
|
490
|
+
const fingerprints = this.fingerprints();
|
|
491
|
+
if (this.saved.documentHashes?.open === hash(this.text) && this.saved.documentHashes?.archived === hash(this.archiveText)) {
|
|
492
|
+
for (const id of Object.keys(fingerprints)) if (this.saved.fingerprints?.[id]) fingerprints[id] = this.saved.fingerprints[id];
|
|
493
|
+
}
|
|
494
|
+
return { documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: { ...fingerprints, ...this.publishedFingerprints }, taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
|
|
467
495
|
}
|
|
468
496
|
|
|
469
497
|
lift() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.111",
|
|
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": {
|