@volter/supercode-orchestrator 0.5.10 → 0.5.12

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.
@@ -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;}
@@ -1,6 +1,6 @@
1
1
  // Public native workflow observation. Filesystem custody authorizes this door;
2
2
  // Teams separately authorizes the Org event and its installed subscribers.
3
- import { watch, existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { watch, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
4
4
  import { dirname, basename, join, resolve } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { listBoards, openBoard, backingPaths } from './store.mjs';
@@ -9,28 +9,39 @@ import { recordCardPublications } from './published.mjs';
9
9
  const identity = root => createHash('sha256').update(realpathSync(root)).digest('hex');
10
10
  const cursorOf = (home, boards) => Buffer.from(JSON.stringify({version:1,home,boards})).toString('base64url');
11
11
  const resync = reason => Object.assign(new Error(reason), {code:'resync_required'});
12
+ const RESETTLE_MS = 30_000;
12
13
 
13
14
  /** Snapshot once, then replay only committed publications on backing notifications. */
14
15
  export async function* followBoardPublications({root,after,signal}) {
15
16
  const home=identity(root), positions={}, watchers=new Map();
16
17
  let dirty=true, wake=null, failure=null, inventory=null;
17
- const notify=()=>{dirty=true;wake?.();};
18
+ // The stores' files as this follower last left them. Its own pass writes (the publication transaction) and closes
19
+ // the board, which rewrites the WAL and raises a watch event of its own; an event that finds every store file as the
20
+ // pass left it is that echo, not a change, and does not start another pass. Without this the follower woke itself
21
+ // every half second and burned a core with nothing to publish (t_0328d520).
22
+ let stores=[], settled=null;
23
+ const stamp=()=>stores.map(path=>{try{const s=statSync(path);return `${s.size}:${s.mtimeMs}`;}catch{return '-';}}).join('|');
24
+ const notify=(name=null)=>{
25
+ if(name!==null&&settled!==null&&stores.some(path=>basename(path)===name)&&stamp()===settled)return;
26
+ dirty=true;wake?.();
27
+ };
18
28
  const directory=(path,accept)=>{
19
29
  if(!existsSync(path))return;
20
30
  if(watchers.has(path)){watchers.get(path).accept.push(accept);return;}
21
31
  const entry={accept:[accept],handle:null};
22
- entry.handle=watch(path,(_kind,name)=>{if(name==null||entry.accept.some(fn=>fn(String(name))))notify();});
32
+ entry.handle=watch(path,(_kind,name)=>{if(name==null)notify();else if(entry.accept.some(fn=>fn(String(name))))notify(String(name));});
23
33
  entry.handle.on('error',error=>{failure=error;notify();});watchers.set(path,entry);
24
34
  };
25
35
  const file=path=>directory(dirname(path),name=>name===basename(path));
26
36
  const register=slugs=>{
27
37
  for(const entry of watchers.values())entry.accept=[];
38
+ stores=[join(root,'kanban.db'),join(root,'kanban.db-wal')];
28
39
  directory(root,name=>['kanban.db','kanban.db-wal','kanban'].includes(name));
29
40
  directory(join(root,'kanban'),name=>['boards','ztrack.json','board'].includes(name));
30
41
  directory(join(root,'kanban','boards'),()=>true);
31
42
  for(const slug of slugs){
32
43
  const paths=backingPaths(root,slug);
33
- file(paths.sqlite);file(`${paths.sqlite}-wal`);file(paths.ztrack);
44
+ file(paths.sqlite);file(`${paths.sqlite}-wal`);file(paths.ztrack);stores.push(paths.sqlite,`${paths.sqlite}-wal`);
34
45
  directory(paths.files,name=>name.endsWith('.json'));
35
46
  try {const cfg=JSON.parse(readFileSync(paths.ztrack,'utf8'));const document=resolve(dirname(paths.ztrack),cfg.document);file(document);file(`${document}.archive.md`);}catch{/* other backing */}
36
47
  }
@@ -83,7 +94,12 @@ export async function* followBoardPublications({root,after,signal}) {
83
94
  }
84
95
  if(initial){initial=false;yield {type:'snapshot_end',cursor:cursorOf(home,positions)};dirty=true;}
85
96
  else if(checkpointPending){checkpointPending=false;yield {type:'checkpoint',cursor:cursorOf(home,positions)};}
86
- if(!dirty&&!signal?.aborted)await new Promise(done=>{wake=done;if(dirty||signal?.aborted)done();});
97
+ settled=stamp();
98
+ // A write that lands between the pass and the stamp above has its event taken for this pass's echo: a pass
99
+ // every RESETTLE_MS anyway bounds how long that write can wait.
100
+ let timer=null;
101
+ if(!dirty&&!signal?.aborted)await new Promise(done=>{wake=done;timer=setTimeout(()=>{dirty=true;done();},RESETTLE_MS);timer.unref?.();if(dirty||signal?.aborted)done();});
102
+ clearTimeout(timer);
87
103
  wake=null;
88
104
  }
89
105
  }finally{signal?.removeEventListener('abort',aborted);for(const {handle} of watchers.values())handle.close();}
@@ -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() { return git(this.root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); }
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');
@@ -34,12 +34,37 @@ export function taskStatusOf(lane, completedAt) {
34
34
  return 'todo';
35
35
  }
36
36
 
37
- export function cardSnapshot(db,row) {
38
- const own=db.prepare('SELECT * FROM supercode_cards WHERE task_id=?').get(row.id)??{};
39
- const comments=db.prepare('SELECT id,author,body,created_at FROM task_comments WHERE task_id=? ORDER BY id').all(row.id).map(comment=>({...comment,created_at:at(comment.created_at)}));
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=db.prepare("SELECT id,kind,payload,created_at FROM task_events WHERE task_id=? AND kind IN ('review_requested','approved','review_approved','changes_requested','escalated') ORDER BY id").all(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)};});
42
- const attempts=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(row.id).map(run=>{
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:db.prepare('SELECT parent_id FROM task_links WHERE child_id=? ORDER BY parent_id').all(row.id).map(link=>link.parent_id),
55
- reviews,children:db.prepare('SELECT child_id FROM task_links WHERE parent_id=? ORDER BY child_id').all(row.id).map(link=>link.child_id),comments,attempts};
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=db.prepare('SELECT digest,last_event_id FROM supercode_publication_heads WHERE resource_id=?').get(row.id);
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 pull request merged on the remote (a page edited in Git Notes) is in the document before this write reads it.
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.10",
3
+ "version": "0.5.12",
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.6",
28
+ "@volter/teams": "0.2.7",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },