@volter/supercode-orchestrator 0.5.162 → 0.5.164

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/cli.mjs CHANGED
@@ -12,6 +12,8 @@ import { BOARD_OPERATION_VERBS, boardHome, boardOwnerSocket, CALLER_FACTS } from
12
12
  import { current, isCallersCall, ownCall, within } from './call.mjs';
13
13
  import { taskStatusOf } from './published.mjs';
14
14
  import { executionOf } from './session-work.mjs';
15
+ import { listCardsInTurns, writeCardList } from './listing.mjs';
16
+ import { writeOwnerAnswer } from './reply.mjs';
15
17
  import { createServer } from 'node:net';
16
18
  import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, copyFileSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
17
19
  import { notRuntime, ownHome } from './owner-files.mjs';
@@ -279,9 +281,6 @@ async function requestedSponsor(args) {
279
281
  return { requestedByUnverified: { observation: args['requested-by'], label: resolved?.label ?? 'unresolved', request: args.request ?? null } };
280
282
  }
281
283
 
282
- // `list --json` reads each card's parents and standing block per card up to this many cards, and in one read of each past it
283
- const LIST_PER_CARD = 64;
284
-
285
284
  const tagsOf = (card) => { const tags = Array.isArray(card.tags) ? card.tags : json(card.tags, null); return Array.isArray(tags) ? tags : card.tenant ? [card.tenant] : []; };
286
285
 
287
286
  const say = (args, human, value) => { current().out(args.json ? `${JSON.stringify(value, null, 2)}\n` : `${human}\n`); };
@@ -354,7 +353,8 @@ export async function runVerb(argv) {
354
353
  // a caller that is a card's own session (its harness names it; the board recorded which session it launched)
355
354
  const listing = ['list','ls'].includes(verb);
356
355
  await loadWorkflow(root).catch((error) => { if (verb !== 'init' && verb !== 'boards') throw error; });
357
- await readAgentMains(root);
356
+ // A listing does not authorize a transition and needs no manager's main session.
357
+ if (!listing) await readAgentMains(root);
358
358
  const sid = listing && !args.mine ? null : callerSession();
359
359
  // what this command records is the calling session's act (its address, as its subscriptions name it)
360
360
  current().recorded = sid && boardExists(root, slug) ? withBoard((b) => b.db.prepare(CALLER_RUN_ADDRESS).get(sid)?.address) ?? null : null;
@@ -503,16 +503,8 @@ export async function runVerb(argv) {
503
503
 
504
504
  case 'list': case 'ls': {
505
505
  const assignee = args.assignee ?? (args.mine ? callerName() : null);
506
- const cards = withBoard((b) => {
507
- let rows = engine.listCards(b.db, { status: args.status, assignee, machine: args.machine, includeArchived: args.archived || args.all });
508
- if (args.tenant) rows = rows.filter((t) => t.tenant === args.tenant);
509
- // a saved view (C4) is the Org's cards that carry its tag
510
- for (const tag of [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])]) rows = rows.filter((t) => tagsOf(t).includes(tag));
511
- if (args.session) rows = rows.filter((t) => t.session_id === args.session);
512
- if (args['workflow-template-id']) rows = rows.filter((t) => t.workflow_template_id === args['workflow-template-id']);
513
- if (args['step-key']) rows = rows.filter((t) => t.current_step_key === args['step-key']);
514
- return rows;
515
- });
506
+ const cards = await withBoardAsync(b => listCardsInTurns(b.db, { detailed: Boolean(args.json), status: args.status, assignee, machine: args.machine, includeArchived: args.archived || args.all,
507
+ tenant: args.tenant, session: args.session, workflowTemplateId: args['workflow-template-id'], stepKey: args['step-key'], tags: [...(viewTag ? [viewTag] : []), ...(args.tag ?? [])] }));
516
508
  const sorters = {
517
509
  assignee: (a, b) => String(a.assignee ?? '').localeCompare(String(b.assignee ?? '')), created: (a, b) => a.created_at - b.created_at,
518
510
  'created-desc': (a, b) => b.created_at - a.created_at, priority: (a, b) => a.priority - b.priority, 'priority-desc': (a, b) => b.priority - a.priority,
@@ -521,44 +513,16 @@ export async function runVerb(argv) {
521
513
  };
522
514
  if (args.sort) { if (!sorters[args.sort]) throw new Error(`--sort is one of ${Object.keys(sorters).join(', ')}`); cards.sort(sorters[args.sort]); }
523
515
  if (args.json) {
524
- // beyond Hermes's fields: the workflow's own status, the cards it waits on and its open run, so a reader of the
525
- // whole board (a board file's sync) needs no `show` per card
526
- const extra = withBoard((b) => {
527
- // A whole board's parents and standing blocks are read once, not once per card. Asked per card, the block's
528
- // read fetched every event row of every card through the card's index to answer from the few of these
529
- // kinds (on 2026-10-11: 45,670 rows of a 17 MB table for 2,154 cards, to find 332), on the owner's event
530
- // loop, for as long as the listing took. A short listing still asks per card: it reads less than the scan.
531
- const whole = cards.length > LIST_PER_CARD;
532
- const parentsOf = new Map(), standing = new Map();
533
- if (whole) {
534
- for (const row of b.db.prepare('SELECT child_id, parent_id FROM task_links ORDER BY child_id, parent_id').all()) {
535
- if (!parentsOf.has(row.child_id)) parentsOf.set(row.child_id, []);
536
- parentsOf.get(row.child_id).push(row.parent_id);
537
- }
538
- // in id order, so the last one kept for a card is its latest
539
- for (const row of b.db.prepare("SELECT task_id, kind, payload FROM task_events WHERE kind IN ('blocked', 'unblocked', 'dependency_wait') ORDER BY id").all()) standing.set(row.task_id, row);
540
- }
541
- return new Map(cards.map((t) => {
542
- const run = t.current_run_id ? b.db.prepare('SELECT id, status, started_at, metadata FROM task_runs WHERE id = ?').get(t.current_run_id) : null;
543
- const session = run ? json(run.metadata, {})?.supercode ?? {} : {};
544
- return [t.id, {
545
- workflow_status: t.stage ?? t.status,
546
- parents: whole ? parentsOf.get(t.id) ?? [] : b.db.prepare('SELECT parent_id FROM task_links WHERE child_id = ? ORDER BY parent_id').all(t.id).map((r) => r.parent_id),
547
- run: run ? { id: run.id, status: run.status, started_at: run.started_at, session: session.address ?? session.session_id ?? null, execution: executionOf(run) } : null,
548
- // the message whose answer its standing block waits for (`block --waiting-on`)
549
- waiting_on: (() => {
550
- const last = whole ? standing.get(t.id) : 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);
551
- return last?.kind === 'blocked' ? json(last.payload, {})?.waiting_on ?? null : null;
552
- })(),
553
- }];
554
- }));
555
- });
556
- say(args, '', cards.map((t) => ({ ...cardDict(t), ...extra.get(t.id) })));
516
+ await writeCardList(cards, t => ({ ...cardDict(t), workflow_status: t.workflow_status, parents: t.parents, run: t.run, waiting_on: t.waiting_on }), current().out);
557
517
  return 0;
558
518
  }
559
519
  const others = boardsOf(root, { includeArchived: false }).length - 1;
560
520
  if (others > 0) current().out(`Board: ${slug} (${others} other board${others === 1 ? '' : 's'} — \`supercode workflow boards list\`)\n\n`);
561
- current().out(cards.length ? `${cards.map(line).join('\n')}\n` : '(no matching tasks)\n');
521
+ if (!cards.length) current().out('(no matching tasks)\n');
522
+ for (let offset = 0; offset < cards.length; offset += 64) {
523
+ current().out(`${cards.slice(offset, offset + 64).map(line).join('\n')}\n`);
524
+ await new Promise(resolve => setImmediate(resolve));
525
+ }
562
526
  return 0;
563
527
  }
564
528
 
@@ -1599,7 +1563,8 @@ async function answerVerbs(root, log) {
1599
1563
  // one answer per call, whatever the request did: a request that fails is a failed answer, never the owner's end
1600
1564
  answerOne(root, line)
1601
1565
  .catch((error) => ({ code: 1, stdout: '', stderr: `supercode workflow: ${error?.message ?? error}\n` }))
1602
- .then((answer) => { if (!socket.destroyed) socket.end(`${JSON.stringify(answer)}\n`); });
1566
+ .then((answer) => writeOwnerAnswer(socket, answer))
1567
+ .catch(() => { socket.destroy(); }); // a disconnected caller leaves no queued reply
1603
1568
  });
1604
1569
  });
1605
1570
  await new Promise((done, fail) => { server.once('error', fail); server.listen(path, () => { server.off('error', fail); done(); }); });
@@ -1642,14 +1607,14 @@ async function answerRequest(root, line, naming) {
1642
1607
  if (!facts.SUPERCODE_CONTEXT) return { code: 1, stdout: '', stderr: 'supercode workflow: the request names no Teams context (facts.SUPERCODE_CONTEXT); nothing was done\n' };
1643
1608
  const cwd = typeof request.cwd === 'string' && request.cwd ? request.cwd : root;
1644
1609
  const stdin = typeof request.stdin === 'string' ? request.stdin : null;
1645
- let stdout = '';
1646
- let stderr = '';
1610
+ const stdout = [];
1611
+ const stderr = [];
1647
1612
  let code;
1648
- await within({ caller, facts, cwd, stdin, out: (text) => { stdout += text; return true; }, err: (text) => { stderr += text; return true; } }, async () => {
1613
+ await within({ caller, facts, cwd, stdin, out: (text) => { stdout.push(text); return true; }, err: (text) => { stderr.push(text); return true; } }, async () => {
1649
1614
  // a worker's board tool (mcp/server.mjs), answered by the board's tools in this call (board/tools.mjs)
1650
- if (tool) { stdout = await boardTool(root, tool.name, tool.args && typeof tool.args === 'object' ? tool.args : {}, Object.fromEntries(Object.entries(tool.env ?? {}).filter(([key, value]) => /^HERMES_(KANBAN_TASK|KANBAN_RUN_ID|KANBAN_BOARD|PROFILE)$/.test(key) && typeof value === 'string'))); code = 0; return; }
1615
+ if (tool) { stdout.push(await boardTool(root, tool.name, tool.args && typeof tool.args === 'object' ? tool.args : {}, Object.fromEntries(Object.entries(tool.env ?? {}).filter(([key, value]) => /^HERMES_(KANBAN_TASK|KANBAN_RUN_ID|KANBAN_BOARD|PROFILE)$/.test(key) && typeof value === 'string')))); code = 0; return; }
1651
1616
  try { code = await runVerb([...argv, '--root', root, ...(typeof request.board === 'string' && request.board ? ['--board', request.board] : [])]); }
1652
- catch (error) { stderr += `supercode workflow: ${error?.message ?? error}\n`; code = 1; }
1617
+ catch (error) { stderr.push(`supercode workflow: ${error?.message ?? error}\n`); code = 1; }
1653
1618
  });
1654
1619
  return { code: code ?? 0, stdout, stderr };
1655
1620
  } catch (error) {
package/board/files.mjs CHANGED
@@ -14,6 +14,7 @@ import { markRemovedHeads } from './published.mjs';
14
14
  import { createHash } from 'node:crypto';
15
15
  import { join } from 'node:path';
16
16
  import { DatabaseSync } from 'node:sqlite';
17
+ import { LIST_REVISION, listRevision } from './listing.mjs';
17
18
 
18
19
  const STATE = '_board.json';
19
20
 
@@ -58,6 +59,7 @@ class FilesBoard {
58
59
  const self = this;
59
60
  // the connection the engine holds: SQLite's statement API over whatever snapshot is current
60
61
  this.db = {
62
+ [LIST_REVISION]: () => self.read(() => listRevision(self.mem)),
61
63
  prepare(sql) {
62
64
  return {
63
65
  get: (...args) => self.read(() => self.statement(sql).get(...args)),
@@ -0,0 +1,134 @@
1
+ // A listing reads at most 64 candidate tasks per turn, even when its filters match none.
2
+ // Native SQLite calls still run on the caller's thread; this is a row bound, not a time bound.
3
+ import { setImmediate as nextTurn } from 'node:timers/promises';
4
+ import { noteStep } from '@volter/supercode-harness-sdk/slow-log';
5
+ import { executionOf } from './session-work.mjs';
6
+
7
+ export const LIST_BATCH = 64;
8
+ export const LIST_CONNECTION = Symbol.for('supercode.board.listing.connection');
9
+ export const LIST_REVISION = Symbol.for('supercode.board.listing.revision');
10
+ const parse = (text, fallback) => { try { return JSON.parse(text) ?? fallback; } catch { return fallback; } };
11
+ const statements = new WeakMap();
12
+ const prepared = (db, sql) => {
13
+ const connection = db[LIST_CONNECTION] ?? db;
14
+ let held = statements.get(connection);
15
+ if (!held) { held = new Map(); statements.set(connection, held); }
16
+ if (!held.has(sql)) held.set(sql, db.prepare(sql));
17
+ return held.get(sql);
18
+ };
19
+ const measured = (name, fn) => {
20
+ const at = performance.now();
21
+ try { return fn(); } finally { noteStep(`board list: ${name}`, performance.now() - at); }
22
+ };
23
+
24
+ // Validate an optimistic read, without leaving a transaction open on the owner's shared
25
+ // connection across awaits. data_version names commits from other connections; total_changes
26
+ // also catches this connection's writes. Schema versions catch TEMP overlay creation/removal.
27
+ // Files backings additionally change connection identity when they reload their projection.
28
+ export function listRevision(db) {
29
+ if (db[LIST_REVISION]) return db[LIST_REVISION]();
30
+ const connection = db[LIST_CONNECTION] ?? db;
31
+ return {
32
+ connection,
33
+ data: prepared(connection, 'PRAGMA main.data_version').get().data_version,
34
+ changes: prepared(connection, 'SELECT total_changes() AS changes').get().changes,
35
+ mainSchema: prepared(connection, 'PRAGMA main.schema_version').get().schema_version,
36
+ tempSchema: prepared(connection, 'PRAGMA temp.schema_version').get().schema_version,
37
+ };
38
+ }
39
+ const sameRevision = (a, b) => Object.keys(a).every(key => a[key] === b[key]);
40
+
41
+ // Only the fields cardDict, human lines, filters and sorters use. A human listing never reads
42
+ // body/result/skills or run history. A JSON listing retains its existing full-text contract.
43
+ const HUMAN_FIELDS = 'id,title,assignee,status,priority,tenant,created_at,started_at,completed_at';
44
+ const JSON_FIELDS = `${HUMAN_FIELDS},body,result,workspace_kind,workspace_path,branch_name,project_id,created_by,skills,max_retries,model_override,provider_override,session_id,workflow_template_id,current_step_key`;
45
+ export const TASK_SUMMARY_INDEX = 'idx_tasks_list_summary';
46
+ export const TASK_SUMMARY_INDEX_SQL = `CREATE INDEX IF NOT EXISTS ${TASK_SUMMARY_INDEX} ON tasks(${HUMAN_FIELDS},current_run_id,session_id,workflow_template_id,current_step_key)`;
47
+ const RUN_METADATA = `CASE WHEN json_valid(r.metadata) THEN json_object('supercode', json_object(
48
+ 'address',json_extract(r.metadata,'$.supercode.address'),'session_id',json_extract(r.metadata,'$.supercode.session_id'),
49
+ 'close_confirmed_at',json_extract(r.metadata,'$.supercode.close_confirmed_at'),'released',json_extract(r.metadata,'$.supercode.released'),
50
+ 'presence_observation',json_extract(r.metadata,'$.supercode.presence_observation'),'idle_observation',json_extract(r.metadata,'$.supercode.idle_observation')
51
+ )) END`;
52
+ export const LIST_CANDIDATES_SQL = `SELECT MAX(id) AS last, json_group_array(id) AS ids FROM (
53
+ SELECT id FROM tasks WHERE id > ? AND id <= ? ORDER BY id LIMIT ${LIST_BATCH})`;
54
+
55
+ // Three reverse index seeks find the latest event of the relevant kinds. Only that row's
56
+ // blocked payload is returned, rather than every historical payload for the whole board.
57
+ export const LAST_BLOCK_SQL = `SELECT CASE WHEN kind = 'blocked' THEN payload END FROM task_events WHERE id = (
58
+ SELECT MAX(id) FROM (
59
+ SELECT (SELECT id FROM task_events WHERE task_id = t.id AND kind = 'blocked' ORDER BY id DESC LIMIT 1) AS id
60
+ UNION ALL SELECT (SELECT id FROM task_events WHERE task_id = t.id AND kind = 'unblocked' ORDER BY id DESC LIMIT 1)
61
+ UNION ALL SELECT (SELECT id FROM task_events WHERE task_id = t.id AND kind = 'dependency_wait' ORDER BY id DESC LIMIT 1)
62
+ ))`;
63
+
64
+ export function listPageSql(detailed, tagCount = 0) {
65
+ return `SELECT t.rowid AS list_order,${(detailed ? JSON_FIELDS : HUMAN_FIELDS).split(',').map(name => `t.${name}`).join(',')},
66
+ c.machine,c.stage${detailed ? ',c.subtask_of' : ''},
67
+ r.id AS run_id,r.status AS run_status,r.started_at AS run_started_at,r.ended_at AS run_ended_at,${RUN_METADATA} AS run_metadata
68
+ ${detailed ? `,(SELECT json_group_array(parent_id) FROM (SELECT parent_id FROM task_links WHERE child_id = t.id ORDER BY parent_id)) AS parents_json,
69
+ (${LAST_BLOCK_SQL}) AS block_payload` : ''}
70
+ FROM tasks t ${detailed ? '' : `INDEXED BY ${TASK_SUMMARY_INDEX}`} LEFT JOIN supercode_cards c ON c.task_id = t.id LEFT JOIN task_runs r ON r.id = t.current_run_id
71
+ WHERE t.id IN (SELECT value FROM json_each(?)) AND (? IS NULL OR t.status = ? OR c.stage = ?)
72
+ AND (? IS NULL OR t.assignee = ?) AND (? IS NULL OR c.machine = ?) AND (? OR t.status != 'archived')
73
+ AND (? IS NULL OR t.tenant = ?) AND (? IS NULL OR t.session_id = ?)
74
+ AND (? IS NULL OR t.workflow_template_id = ?) AND (? IS NULL OR t.current_step_key = ?)
75
+ ${Array.from({ length: tagCount }, () => `AND CASE WHEN json_valid(c.tags) THEN
76
+ CASE WHEN json_type(c.tags) = 'array' THEN EXISTS (SELECT 1 FROM json_each(c.tags) WHERE value = ?) ELSE t.tenant = ? END
77
+ ELSE t.tenant = ? END`).join('\n')}`;
78
+ }
79
+
80
+ export async function listCardsInTurns(db, options = {}) {
81
+ for (let attempt = 0; attempt < 2; attempt++) {
82
+ const revision = measured('read revision', () => listRevision(db));
83
+ const cards = await readCardsInTurns(db, options, revision);
84
+ if (cards !== null) return cards;
85
+ }
86
+ throw Object.assign(new Error('board changed during both listing reads; run list again'), { code: 'board_listing_changed' });
87
+ }
88
+
89
+ async function readCardsInTurns(db, { detailed = false, status = null, assignee = null, machine = null, includeArchived = false,
90
+ tenant = null, session = null, workflowTemplateId = null, stepKey = null, tags = [] }, revision) {
91
+ const out = [];
92
+ const page = prepared(db, listPageSql(detailed, tags.length)), candidates = prepared(db, LIST_CANDIDATES_SQL);
93
+ const ceiling = measured('last candidate', () => prepared(db, "SELECT COALESCE(MAX(id), '') AS cursor FROM tasks").get().cursor);
94
+ let cursor = '';
95
+ while (cursor < ceiling) {
96
+ if (!measured('validate revision', () => sameRevision(revision, listRevision(db)))) return null;
97
+ const range = measured('candidate range', () => candidates.get(cursor, ceiling));
98
+ if (range.last == null) break;
99
+ const rows = measured(detailed ? 'JSON task rows and latest blocks' : 'human task rows', () => page.all(range.ids,
100
+ status, status, status, assignee, assignee, machine, machine, includeArchived ? 1 : 0,
101
+ tenant, tenant, session, session, workflowTemplateId, workflowTemplateId, stepKey, stepKey, ...tags.flatMap(tag => [tag, tag, tag])));
102
+ measured('row projection', () => {
103
+ for (const { run_id, run_status, run_started_at, run_ended_at, run_metadata, parents_json, block_payload, ...task } of rows) {
104
+ const metadata = run_id ? parse(run_metadata, {}) : null;
105
+ const run = run_id ? { id: run_id, status: run_status, started_at: run_started_at, ended_at: run_ended_at, metadata } : null;
106
+ task.execution = executionOf(run);
107
+ if (detailed) {
108
+ task.skills = parse(task.skills, []);
109
+ task.workflow_status = task.stage ?? task.status;
110
+ task.parents = parse(parents_json, []);
111
+ task.waiting_on = parse(block_payload, {})?.waiting_on ?? null;
112
+ task.run = run ? { id: run.id, status: run.status, started_at: run.started_at, session: metadata.supercode?.address ?? metadata.supercode?.session_id ?? null, execution: task.execution } : null;
113
+ }
114
+ out.push(task);
115
+ }
116
+ });
117
+ cursor = range.last;
118
+ await nextTurn();
119
+ }
120
+ if (!measured('validate revision', () => sameRevision(revision, listRevision(db)))) return null;
121
+ // The candidate index uses id order; restore the original task row order for tied sorts.
122
+ measured('ordering', () => out.sort((a, b) => b.priority - a.priority || a.created_at - b.created_at || a.list_order - b.list_order));
123
+ return out;
124
+ }
125
+
126
+ // Keep pretty JSON and its newline while avoiding one whole-board JSON.stringify allocation.
127
+ export async function writeCardList(cards, project, out) {
128
+ out(cards.length ? '[\n' : '[]\n');
129
+ for (let index = 0; index < cards.length; index++) {
130
+ measured('serialize card', () => out(`${JSON.stringify(project(cards[index]), null, 2).split('\n').map(line => ` ${line}`).join('\n')}${index + 1 < cards.length ? ',' : ''}\n`));
131
+ if ((index + 1) % LIST_BATCH === 0) await nextTurn();
132
+ }
133
+ if (cards.length) out(']\n');
134
+ }
@@ -0,0 +1,51 @@
1
+ // The owner's existing one-line answer protocol, escaped and written in bounded pieces.
2
+ // No whole stdout rope is flattened, escaped, or converted to a socket buffer here.
3
+ import { setImmediate as nextTurn } from 'node:timers/promises';
4
+ import { noteStep } from '@volter/supercode-harness-sdk/slow-log';
5
+
6
+ export const REPLY_CHARS = 8192;
7
+ const measured = (name, fn) => {
8
+ const at = performance.now();
9
+ try { return fn(); } finally { noteStep(`owner reply ${name}`, performance.now() - at); }
10
+ };
11
+
12
+ async function write(socket, text) {
13
+ if (socket.destroyed) return false;
14
+ // One buffer in flight. The write callback waits for this write instead of accumulating
15
+ // a whole answer behind a reader that has stopped taking bytes.
16
+ const buffer = measured('encode buffer', () => Buffer.from(text));
17
+ await new Promise((resolve, reject) => {
18
+ const closed = () => finish(new Error('board caller closed before its answer was written'));
19
+ const finish = (error) => { socket.off('close', closed); error ? reject(error) : resolve(); };
20
+ socket.once('close', closed);
21
+ const at = performance.now();
22
+ try { socket.write(buffer, finish); } catch (error) { finish(error); }
23
+ finally { noteStep('owner reply socket.write', performance.now() - at); }
24
+ });
25
+ await nextTurn();
26
+ return !socket.destroyed;
27
+ }
28
+
29
+ export async function writeOwnerAnswer(socket, { code = 0, stdout = '', stderr = '' }) {
30
+ if (!await write(socket, `{"code":${JSON.stringify(code)},"stdout":"`)) return;
31
+ for (const [index, value] of [stdout, stderr].entries()) {
32
+ let carry = '';
33
+ for (const part of Array.isArray(value) ? value : [value]) {
34
+ for (let offset = 0; offset < part.length;) {
35
+ const end = Math.min(part.length, offset + REPLY_CHARS - carry.length);
36
+ let chunk = carry + part.slice(offset, end);
37
+ carry = '';
38
+ offset = end;
39
+ // A pair can straddle either a fragment or an output part. Keep its high half
40
+ // until the next fragment so JSON.stringify emits the original UTF-8 wire bytes.
41
+ const last = chunk.charCodeAt(chunk.length - 1);
42
+ if (last >= 0xd800 && last <= 0xdbff) { carry = chunk.slice(-1); chunk = chunk.slice(0, -1); }
43
+ if (chunk && !await write(socket, measured('escape string chunk', () => JSON.stringify(chunk).slice(1, -1)))) return;
44
+ }
45
+ }
46
+ if (carry && !await write(socket, measured('escape string chunk', () => JSON.stringify(carry).slice(1, -1)))) return;
47
+ if (index === 0 && !await write(socket, '","stderr":"')) return;
48
+ }
49
+ if (!await write(socket, '"}\n')) return;
50
+ measured('socket.end', () => socket.end());
51
+ }
package/board/store.mjs CHANGED
@@ -19,6 +19,7 @@ import { hostname } from 'node:os';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { DatabaseSync } from 'node:sqlite';
21
21
  import { openZtrackBoard } from './ztrack.mjs';
22
+ import { LIST_CONNECTION, TASK_SUMMARY_INDEX, TASK_SUMMARY_INDEX_SQL } from './listing.mjs';
22
23
  import { isFilesBoard, openFilesBoard } from './files.mjs';
23
24
  import { signalBoardWrite } from './wake.mjs';
24
25
  import { PUBLICATION_SCHEMA, pruneDue, prunePublications, publicationTriggers, recordCardPublications } from './published.mjs';
@@ -222,6 +223,20 @@ function layOutEventIndex(db) {
222
223
  }
223
224
  }
224
225
 
226
+ function layOutSummaryIndex(db) {
227
+ if (indexed(db, TASK_SUMMARY_INDEX)) return;
228
+ const report = text => { writeSync(2, text); if (current().kind === 'caller') current().err(text); };
229
+ report(`board: creating index ${TASK_SUMMARY_INDEX}: one-time task scan and summary index write; this synchronous migration holds the owner event loop until it finishes\n`);
230
+ const began = performance.now();
231
+ let made = false;
232
+ try { db.exec(TASK_SUMMARY_INDEX_SQL); made = true; }
233
+ finally {
234
+ const ms = performance.now() - began;
235
+ noteStep(`board open index ${TASK_SUMMARY_INDEX}`, ms);
236
+ report(`board: index ${TASK_SUMMARY_INDEX} ${made ? 'created' : 'creation failed'} after ${Math.round(ms)} ms\n`);
237
+ }
238
+ }
239
+
225
240
  /** Hermes's tables on a fresh board, supercode's own table and columns on every one. */
226
241
  export function layOut(db, { fresh }) {
227
242
  if (fresh) db.exec(SCHEMA);
@@ -234,6 +249,8 @@ export function layOut(db, { fresh }) {
234
249
  // any statement is prepared; no statement names it, so a store without it is only slower.
235
250
  db.exec('CREATE INDEX IF NOT EXISTS idx_supercode_cards_subtask_of ON supercode_cards(subtask_of)');
236
251
  layOutEventIndex(db);
252
+ // A covering summary index keeps human listings out of task records' body overflow pages.
253
+ layOutSummaryIndex(db);
237
254
  // The calling session's run (CALLER_RUN_ADDRESS) and its cards (SESSION_CURRENT_RUNS, SESSION_BOUND, SESSION_RUNS):
238
255
  // without the session's index each lookup read every run's whole metadata, overflow pages included. `id` keeps a
239
256
  // session's runs in the order the lookups ask for, and the address makes the index answer the caller's lookup without
@@ -357,7 +374,7 @@ function heldHandle(entry, slug) {
357
374
  const guard = () => { if (entry.retired) throw Object.assign(new Error(`board ${slug}'s configuration changed while this call held it; nothing was written (read it again)`), { code: 'board_retired' }); };
358
375
  const bound = (target, key) => { const value = target[key]; return typeof value === 'function' ? value.bind(target) : value; };
359
376
  const statement = (st) => new Proxy(st, { get: (target, key) => key === 'run' ? (...args) => { guard(); return target.run(...args); } : bound(target, key) });
360
- const db = new Proxy(entry.board.db, { get: (target, key) => key === 'prepare' ? (sql) => statement(target.prepare(sql)) : key === 'exec' ? (sql) => { guard(); return target.exec(sql); } : bound(target, key) });
377
+ const db = new Proxy(entry.board.db, { get: (target, key) => key === LIST_CONNECTION ? target : key === 'prepare' ? (sql) => statement(target.prepare(sql)) : key === 'exec' ? (sql) => { guard(); return target.exec(sql); } : bound(target, key) });
361
378
  let closed = false;
362
379
  return { ...entry.board, db, tx: (fn) => { guard(); return entry.board.tx(fn); },
363
380
  close() { if (closed) return; closed = true; entry.uses -= 1; if (entry.retired && !entry.uses) try { entry.board.close(); } catch { /* closed */ } } };
package/board/ztrack.mjs CHANGED
@@ -68,6 +68,7 @@ const timedStatement = (sql, fn) => {
68
68
  finally { noteStatement(sql, started, before); }
69
69
  };
70
70
  import { fire, workflowFor } from './workflow.mjs';
71
+ import { LIST_REVISION, listRevision, TASK_SUMMARY_INDEX, TASK_SUMMARY_INDEX_SQL } from './listing.mjs';
71
72
 
72
73
  const require = createRequire(import.meta.url);
73
74
  const hash = (text) => createHash('sha256').update(text).digest('hex');
@@ -171,6 +172,7 @@ class ZtrackBoard {
171
172
  timedStep('board open errors', () => this.loadErrors());
172
173
  const self = this;
173
174
  this.db = {
175
+ [LIST_REVISION]: () => self.read(() => listRevision(self.conn)),
174
176
  boardErrors: self.errors,
175
177
  prepare(sql) {
176
178
  return {
@@ -768,6 +770,7 @@ class ZtrackBoard {
768
770
  if (overlay) {
769
771
  this.dropOverlay();
770
772
  this.conn.exec('CREATE TEMP TABLE tasks AS SELECT * FROM main.tasks; CREATE TEMP TABLE supercode_cards AS SELECT * FROM main.supercode_cards; CREATE TEMP TABLE task_links AS SELECT * FROM main.task_links');
773
+ this.conn.exec(TASK_SUMMARY_INDEX_SQL.replace(TASK_SUMMARY_INDEX, `temp.${TASK_SUMMARY_INDEX}`));
771
774
  this.overlay = true; this.statements.clear();
772
775
  }
773
776
  const initial = !overlay && !this.one('SELECT 1 FROM main.tasks LIMIT 1');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.162",
3
+ "version": "0.5.164",
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.98",
28
- "@volter/teams": "0.2.66",
28
+ "@volter/teams": "0.2.67",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },