@volter/supercode-orchestrator 0.5.157 → 0.5.159
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 +8 -12
- package/board/legacy-store.mjs +70 -0
- package/board/store.mjs +26 -0
- package/board/ztrack.mjs +25 -20
- package/package.json +2 -2
package/board/cli.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
|
26
26
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
29
|
-
import { actorOf, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
|
|
29
|
+
import { actorOf, CALLER_RUN_ADDRESS, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
|
|
30
30
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
31
31
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
32
32
|
import * as engine from './engine.mjs';
|
|
@@ -357,7 +357,7 @@ export async function runVerb(argv) {
|
|
|
357
357
|
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
|
-
current().recorded = sid && boardExists(root, slug) ? withBoard((b) => b.db.prepare(
|
|
360
|
+
current().recorded = sid && boardExists(root, slug) ? withBoard((b) => b.db.prepare(CALLER_RUN_ADDRESS).get(sid)?.address) ?? null : null;
|
|
361
361
|
if (sid) setActor(callerAddress());
|
|
362
362
|
setPrincipal(await callerPrincipal(root, sid, { teams: !sid && !READS.has(verb) }));
|
|
363
363
|
// the dispatcher as the owner holds it, said once per call (owner-health.mjs; nothing is read for it)
|
|
@@ -1590,22 +1590,18 @@ async function answerVerbs(root, log) {
|
|
|
1590
1590
|
|
|
1591
1591
|
/** One request to the owner, answered whole: the verb run in the caller's call, its output kept for the answer. A
|
|
1592
1592
|
* verb that takes more than a second is a slow line of its own, with the steps that ended while it ran (its own and,
|
|
1593
|
-
* in this one process, any other call's that held it) and what the process used
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1593
|
+
* in this one process, any other call's that held it) and what the whole process used over the verb's wall time
|
|
1594
|
+
* (`whole_process`, slow-log.mjs processUse: every thread and every other call in flight, never the verb alone): its
|
|
1595
|
+
* processor time beside the line's `ms` says whether the time was computed or waited for, and the block reads and
|
|
1596
|
+
* major page faults say on what. */
|
|
1596
1597
|
async function answerOne(root, line) {
|
|
1597
1598
|
const began = performance.now();
|
|
1598
|
-
const before =
|
|
1599
|
+
const before = slowLog.processCounters?.() ?? null;
|
|
1599
1600
|
let named = null, by = null;
|
|
1600
1601
|
try { return await answerRequest(root, line, (verb, caller) => { named = verb; by = caller; }); }
|
|
1601
1602
|
finally {
|
|
1602
1603
|
const ms = performance.now() - began;
|
|
1603
|
-
if (named && ms > slowLog.SLOW_MS) {
|
|
1604
|
-
const after = process.resourceUsage();
|
|
1605
|
-
const used = { cpu_ms: Math.round((after.userCPUTime + after.systemCPUTime - before.userCPUTime - before.systemCPUTime) / 1000), major_faults: after.majorPageFault - before.majorPageFault,
|
|
1606
|
-
waits: after.voluntaryContextSwitches - before.voluntaryContextSwitches, preempted: after.involuntaryContextSwitches - before.involuntaryContextSwitches };
|
|
1607
|
-
recordSlow({ kind: 'call', name: `owner verb ${named}`, ms, slowest: null, extra: { caller: by, steps: slowLog.stepsWithin?.(began, performance.now()) ?? null, process: used } });
|
|
1608
|
-
}
|
|
1604
|
+
if (named && ms > slowLog.SLOW_MS) recordSlow({ kind: 'call', name: `owner verb ${named}`, ms, slowest: null, extra: { caller: by, steps: slowLog.stepsWithin?.(began, performance.now()) ?? null, whole_process: before ? slowLog.processUse(before) : null } });
|
|
1609
1605
|
}
|
|
1610
1606
|
}
|
|
1611
1607
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The earlier runtime store beside a board (`ztrack.json.runtime.db`), read for the one thing the board still asks of
|
|
2
|
+
// it after the import: whether the board's store holds its runs (ADR 0003).
|
|
3
|
+
//
|
|
4
|
+
// That store is a WAL file nobody keeps open, so every open of it is a first open: the opener builds the WAL's index
|
|
5
|
+
// under an exclusive lock, and a second process opening in the same moment is refused its first statement with
|
|
6
|
+
// SQLITE_BUSY_RECOVERY ("database is locked"), which SQLite does not wait out. Read-only changes nothing about that.
|
|
7
|
+
// Asked on every board open, two followers opening the board on each pass met there and one of them exited (114
|
|
8
|
+
// times in seven days on one install). So the earlier store's count is read once per process for that store as it
|
|
9
|
+
// stands, and the read is tried again, for a bounded time, when another process is in it.
|
|
10
|
+
//
|
|
11
|
+
// Only the earlier store's half of the answer is kept. The board store's runs are counted on every open, as they
|
|
12
|
+
// always were: its owner keeps it open and writes it through its WAL, so no stamp of its files says its rows are
|
|
13
|
+
// unchanged, and a store that lost runs or was replaced must be refused by the very next open.
|
|
14
|
+
import { statSync } from 'node:fs';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
|
|
17
|
+
// ctime as well as mtime: a file rewritten in place with its mtime put back keeps size and mtime, never ctime
|
|
18
|
+
const stamp = (path, { empty = true } = {}) => { try { const s = statSync(path); return !empty && !s.size ? '' : `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}`; } catch { return null; } };
|
|
19
|
+
// The earlier store as it stands: its file, and its WAL while that holds anything (a write to a WAL file changes the
|
|
20
|
+
// WAL, not always the file; an empty WAL is made and removed by readers).
|
|
21
|
+
const standing = (legacy) => `${stamp(legacy)}|${stamp(`${legacy}-wal`, { empty: false }) ?? ''}`;
|
|
22
|
+
const counted = new Map(); // an earlier store's path → { as: the stamps it was counted under, runs }
|
|
23
|
+
|
|
24
|
+
/** How long a read that keeps meeting another process is tried again before it fails. The meeting this is for (two
|
|
25
|
+
* first opens of the earlier store) lasts as long as building a WAL index: of 631 collided opens measured in two
|
|
26
|
+
* processes that did nothing else, the longest took 20 ms and 4 tries. One second is fifty times that and no longer
|
|
27
|
+
* than a slow line's own bound; a lock held longer is not that meeting, and the opener says so and fails instead of
|
|
28
|
+
* holding its process on it. */
|
|
29
|
+
export const CONTENTION_BOUND_MS = 1000;
|
|
30
|
+
|
|
31
|
+
/** How many runs the earlier store holds (0 when it keeps no runtime rows). */
|
|
32
|
+
function runsOf(legacy) {
|
|
33
|
+
const old = new DatabaseSync(legacy, { readOnly: true });
|
|
34
|
+
try {
|
|
35
|
+
if (!old.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'runtime_rows'").get()) return 0;
|
|
36
|
+
return old.prepare("SELECT COUNT(*) AS n FROM runtime_rows WHERE name = 'task_runs' AND value IS NOT NULL").get().n;
|
|
37
|
+
} finally { old.close(); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `fn`, tried again after `wait(attempt)` while it meets another process (`contended(error)`), for at most
|
|
41
|
+
* CONTENTION_BOUND_MS: then it fails as SQLite failed it (same codes), naming the file and the wait. */
|
|
42
|
+
function bounded(path, fn, { contended, wait }) {
|
|
43
|
+
const began = performance.now();
|
|
44
|
+
for (let attempt = 0; ; attempt++) {
|
|
45
|
+
try { return fn(); }
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!contended(error)) throw error;
|
|
48
|
+
const waited = Math.round(performance.now() - began);
|
|
49
|
+
if (waited >= CONTENTION_BOUND_MS) {
|
|
50
|
+
throw Object.assign(new Error(`database is locked: ${path} was held by another process for ${waited} ms over ${attempt + 1} tries (${error.message}, errcode ${error.errcode})`, { cause: error }), { code: error.code, errcode: error.errcode, errstr: error.errstr });
|
|
51
|
+
}
|
|
52
|
+
wait(attempt);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the board store at `file` holds fewer runs than the earlier store `legacy` beside it: `{ here, runs }` when
|
|
59
|
+
* it does (it was created without the import), null when it does not or no earlier store is there. `countHere` counts
|
|
60
|
+
* the board store's runs, on every call. The earlier store's count is read once per process while its file and WAL
|
|
61
|
+
* stand unchanged. A read that meets another process is tried again within CONTENTION_BOUND_MS (`bounded`).
|
|
62
|
+
*/
|
|
63
|
+
export function unimported(file, legacy, countHere, retry) {
|
|
64
|
+
if (stamp(legacy) === null) return null;
|
|
65
|
+
const here = bounded(file, countHere, retry);
|
|
66
|
+
const as = standing(legacy);
|
|
67
|
+
let known = counted.get(legacy);
|
|
68
|
+
if (known?.as !== as) { known = { as, runs: bounded(legacy, () => runsOf(legacy), retry) }; counted.set(legacy, known); }
|
|
69
|
+
return here < known.runs ? { here, runs: known.runs } : null;
|
|
70
|
+
}
|
package/board/store.mjs
CHANGED
|
@@ -143,6 +143,14 @@ export function listBoards(root) {
|
|
|
143
143
|
return out;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// The run a session's own verb is recorded against, by the session the dispatcher wrote in the run's metadata: asked on
|
|
147
|
+
// every verb a session runs (cli.mjs). The lookup and its index are written from the same two expressions, because
|
|
148
|
+
// SQLite uses an expression index only for a statement that writes the expression the same way.
|
|
149
|
+
const RUN_SESSION = "json_extract(metadata, '$.supercode.session_id')";
|
|
150
|
+
const RUN_ADDRESS = "json_extract(metadata, '$.supercode.address')";
|
|
151
|
+
export const CALLER_RUN_ADDRESS = `SELECT ${RUN_ADDRESS} AS address FROM task_runs WHERE ${RUN_SESSION} = ? ORDER BY id DESC LIMIT 1`;
|
|
152
|
+
const RUN_SESSION_INDEX = 'idx_runs_supercode_session';
|
|
153
|
+
|
|
146
154
|
/** Hermes's tables on a fresh board, supercode's own table and columns on every one. */
|
|
147
155
|
export function layOut(db, { fresh }) {
|
|
148
156
|
if (fresh) db.exec(SCHEMA);
|
|
@@ -154,6 +162,24 @@ export function layOut(db, { fresh }) {
|
|
|
154
162
|
// each read walked every card's row. Laid out here, after the column it names exists on a store of any age and before
|
|
155
163
|
// any statement is prepared; no statement names it, so a store without it is only slower.
|
|
156
164
|
db.exec('CREATE INDEX IF NOT EXISTS idx_supercode_cards_subtask_of ON supercode_cards(subtask_of)');
|
|
165
|
+
// The calling session's run (CALLER_RUN_ADDRESS): without this index each lookup read every run's whole metadata,
|
|
166
|
+
// overflow pages included. `id` keeps a session's runs in the order the lookup asks for, and the address makes the
|
|
167
|
+
// index answer the lookup without reading the run. No statement names it, so a store without it is only slower, and a
|
|
168
|
+
// release that does not know it keeps it current like any other index. Its first creation reads `task_runs` once.
|
|
169
|
+
// A run whose metadata SQLite cannot read as JSON refuses the creation: the board opens without the index, says which
|
|
170
|
+
// rows to look for, and tries again on its next open. Once the index is there, SQLite refuses a write of such
|
|
171
|
+
// metadata to a run.
|
|
172
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'task_runs'").get()) {
|
|
173
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS ${RUN_SESSION_INDEX} ON task_runs(${RUN_SESSION}, id, ${RUN_ADDRESS})`); }
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (contention(error)) throw error;
|
|
176
|
+
const said = current().said;
|
|
177
|
+
if (!said.has(RUN_SESSION_INDEX)) {
|
|
178
|
+
said.add(RUN_SESSION_INDEX);
|
|
179
|
+
current().err(`board: warning: index ${RUN_SESSION_INDEX} on task_runs was not created (${error.message}); the board is open without it and each session's verb reads every run. The runs that refuse it: SELECT id FROM task_runs WHERE metadata IS NOT NULL AND NOT json_valid(metadata, 6)\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
157
183
|
const effectColumns = new Set(db.prepare('PRAGMA table_info(supercode_effects)').all().map((c) => c.name));
|
|
158
184
|
for (const [column, type] of Object.entries(EFFECT_COLUMNS)) if (!effectColumns.has(column)) db.exec(`ALTER TABLE supercode_effects ADD COLUMN ${column} ${type}`);
|
|
159
185
|
// what changed is marked by the store itself, whoever writes it (published.mjs)
|
package/board/ztrack.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
16
16
|
import { DatabaseSync } from 'node:sqlite';
|
|
17
17
|
import { actorOf, contention, event, json, now, principalOf, storageFault, STORAGE_TRIES } from './store.mjs';
|
|
18
18
|
import { GitDocument } from './git-document.mjs';
|
|
19
|
+
import { unimported } from './legacy-store.mjs';
|
|
19
20
|
import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
|
|
20
21
|
import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
21
22
|
|
|
@@ -23,8 +24,11 @@ import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
|
23
24
|
const timedStep = (step, fn) => { const started = performance.now(); try { return fn(); } finally { noteStep(step, performance.now() - started); } };
|
|
24
25
|
// One statement that takes this long is a step of its own, named by its text: a verb that only reads (`show`) runs in
|
|
25
26
|
// no transaction and has no other step, so a hold inside it was a slow line that named no statement (8.7 s on
|
|
26
|
-
// 2026-10-11, where every statement of `show` measures under 80 ms on a copy of the same store).
|
|
27
|
-
|
|
27
|
+
// 2026-10-11, where every statement of `show` measures under 80 ms on a copy of the same store). Every native call on
|
|
28
|
+
// the board's connection is timed this way (ZtrackBoard.statement, the engine's `exec`), the backing's own included:
|
|
29
|
+
// the reads of `board_state` and `board_errors` that come before each of the engine's statements, and a statement's
|
|
30
|
+
// compilation, which reads the schema.
|
|
31
|
+
const SLOW_STATEMENT_MS = 50;
|
|
28
32
|
const timedStatement = (sql, fn) => {
|
|
29
33
|
const started = performance.now();
|
|
30
34
|
try { return fn(); }
|
|
@@ -137,13 +141,13 @@ class ZtrackBoard {
|
|
|
137
141
|
boardErrors: self.errors,
|
|
138
142
|
prepare(sql) {
|
|
139
143
|
return {
|
|
140
|
-
get: (...args) => self.read(() =>
|
|
141
|
-
all: (...args) => self.read(() =>
|
|
142
|
-
iterate: (...args) => self.read(() =>
|
|
143
|
-
run: (...args) => self.active ? self.noting(() =>
|
|
144
|
+
get: (...args) => self.read(() => self.statement(sql).get(...args)),
|
|
145
|
+
all: (...args) => self.read(() => self.statement(sql).all(...args)),
|
|
146
|
+
iterate: (...args) => self.read(() => self.statement(sql).all(...args))[Symbol.iterator](),
|
|
147
|
+
run: (...args) => self.active ? self.noting(() => self.statement(sql).run(...args)) : self.tx(() => self.statement(sql).run(...args)),
|
|
144
148
|
};
|
|
145
149
|
},
|
|
146
|
-
exec(sql) {
|
|
150
|
+
exec(sql) { const run = () => timedStatement(sql, () => self.conn.exec(sql)); return self.active ? self.noting(run) : self.tx(run); },
|
|
147
151
|
close() { self.close(); },
|
|
148
152
|
};
|
|
149
153
|
// A board's first open in this process recovers what its store accepted and derives what git owes for it (tx), so
|
|
@@ -179,7 +183,11 @@ class ZtrackBoard {
|
|
|
179
183
|
|
|
180
184
|
statement(sql) {
|
|
181
185
|
let statement = this.statements.get(sql);
|
|
182
|
-
if (!statement) {
|
|
186
|
+
if (!statement) {
|
|
187
|
+
const prepared = timedStatement(`prepare: ${sql}`, () => this.conn.prepare(sql));
|
|
188
|
+
statement = { get: (...args) => timedStatement(sql, () => prepared.get(...args)), all: (...args) => timedStatement(sql, () => prepared.all(...args)), run: (...args) => timedStatement(sql, () => prepared.run(...args)) };
|
|
189
|
+
this.statements.set(sql, statement);
|
|
190
|
+
}
|
|
183
191
|
return statement;
|
|
184
192
|
}
|
|
185
193
|
one(sql, ...args) { return this.statement(sql).get(...args); }
|
|
@@ -229,18 +237,14 @@ class ZtrackBoard {
|
|
|
229
237
|
// one such store, created by an opener nobody named, sent the dispatcher on to start a second session on every
|
|
230
238
|
// running card). The board refuses it, so nothing runs on a board without its history.
|
|
231
239
|
refuseUnimported(legacy) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
runs = old.prepare("SELECT COUNT(*) AS n FROM runtime_rows WHERE name = 'task_runs' AND value IS NOT NULL").get().n;
|
|
239
|
-
}
|
|
240
|
-
} finally { old.close(); }
|
|
241
|
-
if (here >= runs) return;
|
|
240
|
+
// the earlier store's count is read once per process while that store stands unchanged, and a read that meets
|
|
241
|
+
// another process is tried again for a bounded time (legacy-store.mjs): the earlier store is opened by nobody
|
|
242
|
+
// else, so two openers at once refuse each other. This store's own runs are counted on every open.
|
|
243
|
+
const short = unimported(this.file, legacy, () => this.conn.prepare('SELECT COUNT(*) AS n FROM main.task_runs').get().n,
|
|
244
|
+
{ contended: sqliteContention, wait: (attempt) => timedStep('board open: earlier store contention backoff', () => backoff(attempt)) });
|
|
245
|
+
if (!short) return;
|
|
242
246
|
this.conn.close();
|
|
243
|
-
throw new Error(`${this.file} holds ${here} runs, but ${legacy} holds ${runs}: it was created without importing them. Move it aside (keep it; writes made to it are not in the earlier store) and run any board command to import.`);
|
|
247
|
+
throw new Error(`${this.file} holds ${short.here} runs, but ${legacy} holds ${short.runs}: it was created without importing them. Move it aside (keep it; writes made to it are not in the earlier store) and run any board command to import.`);
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
// A board created from the previous runtime store imports its rows once; the old file stays as it was.
|
|
@@ -352,7 +356,8 @@ class ZtrackBoard {
|
|
|
352
356
|
if (this.active) return this.noting(fn);
|
|
353
357
|
this.observeForRead();
|
|
354
358
|
for (let attempt = 0; ; attempt++) {
|
|
355
|
-
|
|
359
|
+
// each sleep between tries is a step: its count on a slow line is how many times the read was tried again
|
|
360
|
+
try { return fn(); } catch (error) { if (!sqliteContention(error)) throw error; timedStep('board read: contention backoff', () => backoff(attempt)); }
|
|
356
361
|
}
|
|
357
362
|
}
|
|
358
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.159",
|
|
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": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"check-types": "node scripts/generate-types.mjs --check"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@volter/supercode-harness-sdk": "^0.3.
|
|
27
|
+
"@volter/supercode-harness-sdk": "^0.3.97",
|
|
28
28
|
"@volter/teams": "0.2.65",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|