@volter/supercode-orchestrator 0.5.156 → 0.5.158
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 +6 -2
- package/board/legacy-store.mjs +70 -0
- package/board/ztrack.mjs +28 -14
- package/package.json +2 -2
package/board/cli.mjs
CHANGED
|
@@ -1590,14 +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)
|
|
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. */
|
|
1594
1597
|
async function answerOne(root, line) {
|
|
1595
1598
|
const began = performance.now();
|
|
1599
|
+
const before = slowLog.processCounters?.() ?? null;
|
|
1596
1600
|
let named = null, by = null;
|
|
1597
1601
|
try { return await answerRequest(root, line, (verb, caller) => { named = verb; by = caller; }); }
|
|
1598
1602
|
finally {
|
|
1599
1603
|
const ms = performance.now() - began;
|
|
1600
|
-
if (named) recordSlow({ kind: 'call', name: `owner verb ${named}`, ms, slowest: null, extra: { caller: by, steps: slowLog.stepsWithin?.(began, performance.now()) ?? null } });
|
|
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 } });
|
|
1601
1605
|
}
|
|
1602
1606
|
}
|
|
1603
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/ztrack.mjs
CHANGED
|
@@ -16,11 +16,24 @@ 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
|
|
|
22
23
|
/** Run `fn` as a named step of the running command (the slow log names its slowest step). */
|
|
23
24
|
const timedStep = (step, fn) => { const started = performance.now(); try { return fn(); } finally { noteStep(step, performance.now() - started); } };
|
|
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
|
|
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
|
|
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;
|
|
32
|
+
const timedStatement = (sql, fn) => {
|
|
33
|
+
const started = performance.now();
|
|
34
|
+
try { return fn(); }
|
|
35
|
+
finally { const ms = performance.now() - started; if (ms >= SLOW_STATEMENT_MS) noteStep(`board statement: ${String(sql).replace(/\s+/g, ' ').trim().slice(0, 120)}`, ms); }
|
|
36
|
+
};
|
|
24
37
|
import { fire, workflowFor } from './workflow.mjs';
|
|
25
38
|
|
|
26
39
|
const require = createRequire(import.meta.url);
|
|
@@ -134,7 +147,7 @@ class ZtrackBoard {
|
|
|
134
147
|
run: (...args) => self.active ? self.noting(() => self.statement(sql).run(...args)) : self.tx(() => self.statement(sql).run(...args)),
|
|
135
148
|
};
|
|
136
149
|
},
|
|
137
|
-
exec(sql) {
|
|
150
|
+
exec(sql) { const run = () => timedStatement(sql, () => self.conn.exec(sql)); return self.active ? self.noting(run) : self.tx(run); },
|
|
138
151
|
close() { self.close(); },
|
|
139
152
|
};
|
|
140
153
|
// A board's first open in this process recovers what its store accepted and derives what git owes for it (tx), so
|
|
@@ -170,7 +183,11 @@ class ZtrackBoard {
|
|
|
170
183
|
|
|
171
184
|
statement(sql) {
|
|
172
185
|
let statement = this.statements.get(sql);
|
|
173
|
-
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
|
+
}
|
|
174
191
|
return statement;
|
|
175
192
|
}
|
|
176
193
|
one(sql, ...args) { return this.statement(sql).get(...args); }
|
|
@@ -220,18 +237,14 @@ class ZtrackBoard {
|
|
|
220
237
|
// one such store, created by an opener nobody named, sent the dispatcher on to start a second session on every
|
|
221
238
|
// running card). The board refuses it, so nothing runs on a board without its history.
|
|
222
239
|
refuseUnimported(legacy) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
runs = old.prepare("SELECT COUNT(*) AS n FROM runtime_rows WHERE name = 'task_runs' AND value IS NOT NULL").get().n;
|
|
230
|
-
}
|
|
231
|
-
} finally { old.close(); }
|
|
232
|
-
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;
|
|
233
246
|
this.conn.close();
|
|
234
|
-
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.`);
|
|
235
248
|
}
|
|
236
249
|
|
|
237
250
|
// A board created from the previous runtime store imports its rows once; the old file stays as it was.
|
|
@@ -343,7 +356,8 @@ class ZtrackBoard {
|
|
|
343
356
|
if (this.active) return this.noting(fn);
|
|
344
357
|
this.observeForRead();
|
|
345
358
|
for (let attempt = 0; ; attempt++) {
|
|
346
|
-
|
|
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)); }
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
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.158",
|
|
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"
|