@volter/supercode-orchestrator 0.5.10 → 0.5.11
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/bin/orchestrator.mjs +4 -1
- package/board/git-document.mjs +14 -1
- package/board/published.mjs +36 -10
- package/board/ztrack.mjs +16 -4
- package/package.json +2 -2
package/bin/orchestrator.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Board commands do not load daemon transports, platform adapters or the MCP server.
|
|
3
3
|
if (process.argv[2] === 'workflow') {
|
|
4
|
-
const [{runWorkflow},{closeWorld}]=await Promise.all([import('../board/cli.mjs'),import('../orchestration.mjs')]);
|
|
4
|
+
const [{runWorkflow},{closeWorld},{timeCommand}]=await Promise.all([import('../board/cli.mjs'),import('../orchestration.mjs'),import('@volter/supercode-harness-sdk/slow-log')]);
|
|
5
|
+
// A board command is timed into the slow log; `serve` runs until stopped and is not one.
|
|
6
|
+
const verb=process.argv.slice(3).find((arg,index,all)=>!arg.startsWith('-')&&!(index>0&&/^--(root|board|cwd|context|at)$/.test(all[index-1])));
|
|
7
|
+
if(verb!=='serve')timeCommand(`workflow ${verb??''}`.trim(),process.argv.slice(3));
|
|
5
8
|
let code=0;
|
|
6
9
|
try {code=await runWorkflow(process.argv.slice(3));}
|
|
7
10
|
catch(error) {process.stderr.write(`supercode workflow: ${error.message??error}\n`);code=1;}
|
package/board/git-document.mjs
CHANGED
|
@@ -33,12 +33,24 @@ export class GitDocument {
|
|
|
33
33
|
|
|
34
34
|
get usable() { return this.root !== null; }
|
|
35
35
|
rel(path) { return relative(this.root, path); }
|
|
36
|
-
branch
|
|
36
|
+
/** The checkout's branch, or null on no branch (a detached HEAD has nowhere to push and nothing to pull). */
|
|
37
|
+
branch() {
|
|
38
|
+
const name = git(this.root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
39
|
+
return name && name !== 'HEAD' ? name : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Said once per process: a checkout on no branch keeps its commits here (every write tried to push `HEAD:HEAD`
|
|
43
|
+
* three times, with a fetch and a merge between, and failed: seconds of network on each write, t_23e47b73). */
|
|
44
|
+
detached() {
|
|
45
|
+
if (!this.warnedDetached) process.stderr.write(`board: the board documents' checkout ${this.root} is on no branch; writes are committed there and not pushed until it is on one\n`);
|
|
46
|
+
this.warnedDetached = true;
|
|
47
|
+
}
|
|
37
48
|
|
|
38
49
|
// Before a write: take what was pushed, so a merged pull request's edit is the document the write starts from.
|
|
39
50
|
pull() {
|
|
40
51
|
if (!this.usable) return;
|
|
41
52
|
const branch = this.branch();
|
|
53
|
+
if (!branch) return this.detached();
|
|
42
54
|
if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) return; // offline: the next write tries again
|
|
43
55
|
const behind = Number(git(this.root, ['rev-list', '--count', `HEAD..origin/${branch}`]).trim());
|
|
44
56
|
if (behind === 0) return;
|
|
@@ -59,6 +71,7 @@ export class GitDocument {
|
|
|
59
71
|
const who = doer || 'board';
|
|
60
72
|
indexed(this.root, ['-c', `user.name=${who}`, '-c', 'user.email=board@supercode.invalid', 'commit', '--quiet', '--author', `${who} <board@supercode.invalid>`, '-m', message, '--', ...files]);
|
|
61
73
|
const branch = this.branch();
|
|
74
|
+
if (!branch) return this.detached();
|
|
62
75
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
63
76
|
if (quiet(this.root, ['push', '--quiet', 'origin', `HEAD:${branch}`]) !== null) return;
|
|
64
77
|
if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) throw new Error('the board documents were committed here but could not be pushed: the remote did not answer');
|
package/board/published.mjs
CHANGED
|
@@ -34,12 +34,37 @@ export function taskStatusOf(lane, completedAt) {
|
|
|
34
34
|
return 'todo';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const REVIEW_KINDS="('review_requested','approved','review_approved','changes_requested','escalated')";
|
|
38
|
+
/** A card's rows read one card at a time. */
|
|
39
|
+
const perCard=db=>({
|
|
40
|
+
own:id=>db.prepare('SELECT * FROM supercode_cards WHERE task_id=?').get(id),
|
|
41
|
+
comments:id=>db.prepare('SELECT id,author,body,created_at FROM task_comments WHERE task_id=? ORDER BY id').all(id),
|
|
42
|
+
reviews:id=>db.prepare(`SELECT id,kind,payload,created_at FROM task_events WHERE task_id=? AND kind IN ${REVIEW_KINDS} ORDER BY id`).all(id),
|
|
43
|
+
attempts:id=>db.prepare('SELECT id,profile,step_key,status,started_at,ended_at,outcome,summary,metadata,error FROM task_runs WHERE task_id=? ORDER BY id').all(id),
|
|
44
|
+
parents:id=>db.prepare('SELECT parent_id FROM task_links WHERE child_id=? ORDER BY parent_id').all(id).map(link=>link.parent_id),
|
|
45
|
+
children:id=>db.prepare('SELECT child_id FROM task_links WHERE parent_id=? ORDER BY child_id').all(id).map(link=>link.child_id),
|
|
46
|
+
});
|
|
47
|
+
/** Every card's rows read in one query per table: what a write that snapshots the whole board reads (one query per card
|
|
48
|
+
* and table was most of each write's transaction, 470 ms for this board's 1,553 cards, t_23e47b73). */
|
|
49
|
+
function wholeBoard(db){
|
|
50
|
+
const grouped=(sql,pick)=>{const by=new Map();for(const row of db.prepare(sql).all()){const list=by.get(row.task_id);const value=pick(row);if(list)list.push(value);else by.set(row.task_id,[value]);}return id=>by.get(id)??[];};
|
|
51
|
+
const own=new Map(db.prepare('SELECT * FROM supercode_cards').all().map(row=>[row.task_id,row]));
|
|
52
|
+
return {
|
|
53
|
+
own:id=>own.get(id),
|
|
54
|
+
comments:grouped('SELECT task_id,id,author,body,created_at FROM task_comments ORDER BY id',({id,author,body,created_at})=>({id,author,body,created_at})),
|
|
55
|
+
reviews:grouped(`SELECT task_id,id,kind,payload,created_at FROM task_events WHERE kind IN ${REVIEW_KINDS} ORDER BY id`,({id,kind,payload,created_at})=>({id,kind,payload,created_at})),
|
|
56
|
+
attempts:grouped('SELECT task_id,id,profile,step_key,status,started_at,ended_at,outcome,summary,metadata,error FROM task_runs ORDER BY id',({task_id:_,...run})=>run),
|
|
57
|
+
parents:grouped('SELECT child_id AS task_id,parent_id FROM task_links ORDER BY parent_id',link=>link.parent_id),
|
|
58
|
+
children:grouped('SELECT parent_id AS task_id,child_id FROM task_links ORDER BY child_id',link=>link.child_id),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cardSnapshot(db,row,rows=perCard(db)) {
|
|
63
|
+
const own=rows.own(row.id)??{};
|
|
64
|
+
const comments=rows.comments(row.id).map(comment=>({...comment,created_at:at(comment.created_at)}));
|
|
40
65
|
const verdicts={review_requested:'requested',approved:'approved',review_approved:'approved',changes_requested:'changes_requested',escalated:'escalated'};
|
|
41
|
-
const reviews=
|
|
42
|
-
const attempts=
|
|
66
|
+
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)};});
|
|
67
|
+
const attempts=rows.attempts(row.id).map(run=>{
|
|
43
68
|
const native=parse(run.metadata,{})?.supercode??null;
|
|
44
69
|
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;
|
|
45
70
|
const metadata=parse(run.metadata,{});delete metadata.supercode;
|
|
@@ -51,16 +76,17 @@ export function cardSnapshot(db,row) {
|
|
|
51
76
|
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,
|
|
52
77
|
blocked_by:parse(own.blocked_by,[]),paused:parse(own.paused),every:own.every??null,
|
|
53
78
|
workspace:{kind:row.workspace_kind,path:row.workspace_path,branch:row.branch_name,root:own.workspace_root,anchor:own.workspace_anchor},
|
|
54
|
-
parents:
|
|
55
|
-
reviews,children:
|
|
79
|
+
parents:rows.parents(row.id),
|
|
80
|
+
reviews,children:rows.children(row.id),comments,attempts};
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
/** Runs inside the backing's native transaction, including files and ztrack projections. */
|
|
59
84
|
export function recordCardPublications(db,actor) {
|
|
60
|
-
const rows=db.prepare('SELECT * FROM tasks ORDER BY id').all(),present=new Set();let changed=0;
|
|
85
|
+
const rows=db.prepare('SELECT * FROM tasks ORDER BY id').all(),present=new Set(),board=wholeBoard(db);let changed=0;
|
|
86
|
+
const heads=new Map(db.prepare('SELECT resource_id,digest,last_event_id FROM supercode_publication_heads').all().map(head=>[head.resource_id,head]));
|
|
61
87
|
for(const row of rows){
|
|
62
|
-
present.add(row.id);const snapshot=JSON.stringify(cardSnapshot(db,row)),digest=hash(snapshot);
|
|
63
|
-
const previous=
|
|
88
|
+
present.add(row.id);const snapshot=JSON.stringify(cardSnapshot(db,row,board)),digest=hash(snapshot);
|
|
89
|
+
const previous=heads.get(row.id);
|
|
64
90
|
if(previous?.digest===digest)continue;
|
|
65
91
|
const last=db.prepare('SELECT id,kind FROM task_events WHERE task_id=? ORDER BY id DESC LIMIT 1').get(row.id);
|
|
66
92
|
const kind=`task.${previous?(last?.id!==previous.last_event_id?kinds[last?.kind]??'changed':'changed'):'created'}`;
|
package/board/ztrack.mjs
CHANGED
|
@@ -14,6 +14,10 @@ import { basename, dirname, resolve } from 'node:path';
|
|
|
14
14
|
import { DatabaseSync } from 'node:sqlite';
|
|
15
15
|
import { actorOf, event, json, now, principalOf, setActor } from './store.mjs';
|
|
16
16
|
import { GitDocument } from './git-document.mjs';
|
|
17
|
+
import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
|
|
18
|
+
|
|
19
|
+
/** Run `fn` as a named step of the running command (the slow log names its slowest step). */
|
|
20
|
+
const timedStep = (step, fn) => { const started = performance.now(); try { return fn(); } finally { noteStep(step, performance.now() - started); } };
|
|
17
21
|
import { fire, statusOf, storedStatus, workflowFor } from './workflow.mjs';
|
|
18
22
|
import { completionPhase, inspectCompletionEvidence, withCompletionEvidence } from './ci.mjs';
|
|
19
23
|
|
|
@@ -333,14 +337,17 @@ class ZtrackBoard {
|
|
|
333
337
|
// ------------------------------------------------------------------ writes
|
|
334
338
|
|
|
335
339
|
tx(fn) {
|
|
336
|
-
// A
|
|
337
|
-
try { this.git?.pull(); } catch (error) { process.stderr.write(`board: ${error.message}\n`); }
|
|
340
|
+
// A write inside a write is part of it: the pull below happened once, for the outermost one.
|
|
338
341
|
if (this.active) return fn(this.db);
|
|
342
|
+
// A pull request merged on the remote (a page edited in Git Notes) is in the document before this write reads it.
|
|
343
|
+
try { timedStep('board git pull', () => this.git?.pull()); } catch (error) { process.stderr.write(`board: ${error.message}\n`); }
|
|
344
|
+
let txStarted = 0;
|
|
339
345
|
for (let attempt = 0; ; attempt++) {
|
|
340
346
|
const hermes = this.seed ? null : this.prepareHermes();
|
|
341
347
|
this.dropOverlay();
|
|
342
348
|
let outcome;
|
|
343
349
|
try {
|
|
350
|
+
txStarted = performance.now();
|
|
344
351
|
this.conn.exec('BEGIN');
|
|
345
352
|
this.active = true;
|
|
346
353
|
this.conn.exec('DELETE FROM temp.board_dirty; DELETE FROM temp.board_touched');
|
|
@@ -364,11 +371,16 @@ class ZtrackBoard {
|
|
|
364
371
|
this.put('pending_hashes', Object.fromEntries(files.map((f) => [f.role, { before: f.before, after: f.after }])));
|
|
365
372
|
}
|
|
366
373
|
this.conn.exec('COMMIT');
|
|
374
|
+
// A long transaction holds every other writer of this board (the dispatcher's tick included), so it is a line
|
|
375
|
+
// of its own in the slow log, from whichever process ran it.
|
|
376
|
+
const held = performance.now() - txStarted;
|
|
377
|
+
noteStep('board transaction', held);
|
|
378
|
+
recordSlow({ kind: 'call', name: 'board transaction', ms: held, slowest: null, extra: { board: this.path } });
|
|
367
379
|
outcome = { result, files, intent };
|
|
368
380
|
} catch (error) {
|
|
369
381
|
try { this.conn.exec('ROLLBACK'); } catch { /* not begun */ }
|
|
370
382
|
this.active = false; this.seen = null; this.loadErrors();
|
|
371
|
-
if (error === RETRY || sqliteContention(error)) { backoff(attempt); continue; }
|
|
383
|
+
if (error === RETRY || sqliteContention(error)) { backoff(attempt); noteStep('board lock wait', performance.now() - txStarted); continue; }
|
|
372
384
|
throw error;
|
|
373
385
|
} finally { this.active = false; }
|
|
374
386
|
this.seed = null;
|
|
@@ -376,7 +388,7 @@ class ZtrackBoard {
|
|
|
376
388
|
this.publish(outcome);
|
|
377
389
|
// Committed as the session (or person) whose act this write was, and pushed.
|
|
378
390
|
const byManager = this.manager !== null && (actorOf() === this.manager || principalOf() === this.manager);
|
|
379
|
-
if (!byManager) try { this.git?.commit(actorOf(), `board: ${outcome.files.map((file) => basename(file.path)).join(', ')} written for ${actorOf() ?? 'the board'}`); }
|
|
391
|
+
if (!byManager) try { timedStep('board git commit and push', () => this.git?.commit(actorOf(), `board: ${outcome.files.map((file) => basename(file.path)).join(', ')} written for ${actorOf() ?? 'the board'}`)); }
|
|
380
392
|
catch (error) { process.stderr.write(`board: ${error.message}\n`); }
|
|
381
393
|
}
|
|
382
394
|
this.exportFollowing();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.41",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.7",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|