@volter/supercode-orchestrator 0.5.224 → 0.5.225
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 +4 -5
- package/board/files.mjs +1 -4
- package/board/generation-owner.mjs +4 -1
- package/board/storage-contract.mjs +85 -0
- package/board/store.mjs +0 -12
- package/board/ztrack.mjs +4 -10
- package/package.json +2 -2
package/board/cli.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
|
35
35
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
36
36
|
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
38
|
+
import { storageContract } from './storage-contract.mjs';
|
|
38
39
|
import { boardKey, cardRef, actorOf, CALLER_RUN_ADDRESS, CALLER_RUN_AGENT, SESSION_CURRENT_RUNS, SESSION_RUNS, sessionIndexed, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardCopyOf, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
|
|
39
40
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
40
41
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
@@ -1970,11 +1971,9 @@ export async function serveBoardGeneration(root, args = {}, argv = ['serve', '--
|
|
|
1970
1971
|
};
|
|
1971
1972
|
const disconnected = () => { stopDispatching(); process.exit(1); };
|
|
1972
1973
|
process.on('message', receive); process.once('disconnect', disconnected);
|
|
1973
|
-
// A transition cannot restore database files over committed card writes
|
|
1974
|
-
//
|
|
1975
|
-
|
|
1976
|
-
for (const file of ['store.mjs', 'files.mjs', 'ztrack.mjs', 'published.mjs']) storage.update(file).update(readFileSync(new URL(file, import.meta.url)));
|
|
1977
|
-
tellOwner({ type: 'prepared', storage: storage.digest('hex') });
|
|
1974
|
+
// A transition cannot restore database files over committed card writes: retained generations must write the same
|
|
1975
|
+
// storage format (storage-contract.mjs), and a format change waits for a compatible migration with its own protocol.
|
|
1976
|
+
tellOwner({ type: 'prepared', ...storageContract() });
|
|
1978
1977
|
try { await activation; return await runVerb(argv); }
|
|
1979
1978
|
finally { process.off('message', receive); process.off('disconnect', disconnected); unwatchHealth(); unwatchWorkflow(); }
|
|
1980
1979
|
}
|
package/board/files.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { fillReadableIds } from './readable-id.mjs';
|
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { fenced } from './fence.mjs';
|
|
14
14
|
import { markRemovedHeads } from './published.mjs';
|
|
15
|
-
import { CARD_NUMBERS
|
|
15
|
+
import { CARD_NUMBERS } from './store.mjs';
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -133,9 +133,6 @@ class FilesBoard {
|
|
|
133
133
|
cards.sort((a, b) => madeAt(a) - madeAt(b) || (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0));
|
|
134
134
|
for (const card of cards) for (const [table, rows] of Object.entries(card.tables ?? {})) if (table !== CARD_NUMBERS) insert(table, rows);
|
|
135
135
|
for (const [table, rows] of Object.entries(state?.tables ?? {})) insert(table, rows);
|
|
136
|
-
// A task whose file names no number keeps none: only cards are numbered.
|
|
137
|
-
unnumberTasks(mem, new Set(cards.filter((card) => card.tables?.[CARD_NUMBERS]?.length).map((card) => card.id)),
|
|
138
|
-
(state?.sequence ?? []).find((row) => row.name === CARD_NUMBERS)?.seq ?? 0);
|
|
139
136
|
// Loading inserts every card, which marks every card changed (published.mjs). The first load of this projection keeps
|
|
140
137
|
// those marks: its next write compares each card with its published head once (a named limit: each projection opened
|
|
141
138
|
// compares every card once, as each load already reads every card's file). A reload keeps the marks not yet
|
|
@@ -199,7 +199,10 @@ export async function serveBoardOwner(root, args) {
|
|
|
199
199
|
const candidate = start(selected);
|
|
200
200
|
try {
|
|
201
201
|
const proof = await candidate.wait('prepared'), held = active.events.get('prepared');
|
|
202
|
-
|
|
202
|
+
// Both generations name their format (storage-contract.mjs): the formats are compared. A generation from before
|
|
203
|
+
// `format` names only its storage modules' source, which is then compared.
|
|
204
|
+
const same = proof.format && held?.format ? proof.format === held.format : Boolean(proof.storage) && proof.storage === held?.storage;
|
|
205
|
+
if (!same) throw new Error('board storage contract differs; predecessor retained without opening a writer');
|
|
203
206
|
pending = { id, candidate, predecessor: active, older: previous, phase: 'prepared' };
|
|
204
207
|
return status();
|
|
205
208
|
} catch (error) { await terminate(candidate); throw error; }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The board's storage contract, which a retained handover compares between predecessor and candidate (ADR 0080,
|
|
2
|
+
// generation-owner.mjs prepare). A rollback leaves the predecessor serving what the candidate wrote, so the two must
|
|
3
|
+
// write the same format.
|
|
4
|
+
// - `format` fails closed. It is the SQL every backing lays out, as SQLite records it once the layout has run on an
|
|
5
|
+
// empty database, and the whole source of the four storage modules except what a format cannot depend on: their
|
|
6
|
+
// import lines, whole-line comments, blank lines, and the members named in READS, which decide how a store is read or
|
|
7
|
+
// which rows a load keeps and write no format. Any other change to those modules moves it, a writer or anything a
|
|
8
|
+
// writer reads (the property sets, a file name, a number's form) alike.
|
|
9
|
+
// - `storage` is the earlier contract, the modules' whole source. An owner from before `format` compares it alone, so a
|
|
10
|
+
// candidate keeps sending it; an owner that knows `format` compares that when both generations send one.
|
|
11
|
+
// `format` is null when a member READS names occurs more than once (the owner then compares `storage`).
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import fs, { readFileSync } from 'node:fs';
|
|
14
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import { layOut } from './store.mjs';
|
|
17
|
+
|
|
18
|
+
const MODULES = ['store.mjs', 'files.mjs', 'ztrack.mjs', 'published.mjs'];
|
|
19
|
+
// Members that only read a store or choose which rows a load keeps (VO-3578 row 22's #2328 changed these): a change to
|
|
20
|
+
// one keeps the format. A member joins this list only when what it writes is rows of the laid-out tables, never a file,
|
|
21
|
+
// a document's text or a JSON shape.
|
|
22
|
+
const READS = {
|
|
23
|
+
'store.mjs': ['unnumberTasks'],
|
|
24
|
+
'files.mjs': ['load'],
|
|
25
|
+
'ztrack.mjs': ['applyPlan', 'importRuntime'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const source = (file) => readFileSync(new URL(file, import.meta.url), 'utf8');
|
|
29
|
+
// A member's lines in its module: from its first line (` name(` in a class, `function name(` or `const name =` at the
|
|
30
|
+
// top) to the line that closes it at the same indent, or that line alone when it ends the member. None when absent.
|
|
31
|
+
function without(text, name) {
|
|
32
|
+
const lines = text.split('\n'), opens = new RegExp(`^(?: (?:async )?${name}\\(|(?:export )?(?:async )?function ${name}\\(|(?:export )?const ${name} = )`);
|
|
33
|
+
const starts = lines.flatMap((line, i) => opens.test(line) ? [i] : []);
|
|
34
|
+
if (starts.length > 1) throw new Error(`${name} is more than one member of its module`);
|
|
35
|
+
if (!starts.length) return text;
|
|
36
|
+
const [start] = starts, first = lines[start].trimEnd(), indent = /^ */.exec(first)[0];
|
|
37
|
+
const end = first.endsWith(';') ? start : lines.findIndex((line, i) => i > start && line === `${indent}}`);
|
|
38
|
+
if (end < 0) throw new Error(`${name} has no closing line`);
|
|
39
|
+
return [...lines.slice(0, start), ...lines.slice(end + 1)].join('\n');
|
|
40
|
+
}
|
|
41
|
+
// What a format cannot depend on: import statements, lines that are only a comment, and blank lines.
|
|
42
|
+
function code(text) {
|
|
43
|
+
const kept = [];
|
|
44
|
+
let importing = false;
|
|
45
|
+
for (const line of text.split('\n')) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (importing || /^import\s/.test(trimmed)) { importing = !/;\s*$/.test(trimmed); continue; }
|
|
48
|
+
if (!trimmed || /^(\/\/|\/\*|\*)/.test(trimmed)) continue;
|
|
49
|
+
kept.push(line);
|
|
50
|
+
}
|
|
51
|
+
return kept.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The layout's one-time migration notices (store.mjs writes them to stderr with writeSync) describe a real store; on
|
|
55
|
+
// this empty one they would put 'holds the owner event loop' lines in the board's log at every generation start. The
|
|
56
|
+
// layout is synchronous, so for its call alone a stderr line that starts 'board: ' is dropped; writeSync is restored
|
|
57
|
+
// before anything else runs.
|
|
58
|
+
function quietly(work) {
|
|
59
|
+
const writeSync = fs.writeSync;
|
|
60
|
+
fs.writeSync = (fd, text, ...rest) => (fd === 2 && typeof text === 'string' && text.startsWith('board: ') ? text.length : writeSync(fd, text, ...rest));
|
|
61
|
+
syncBuiltinESMExports();
|
|
62
|
+
try { return work(); } finally { fs.writeSync = writeSync; syncBuiltinESMExports(); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function format() {
|
|
66
|
+
const db = new DatabaseSync(':memory:');
|
|
67
|
+
try {
|
|
68
|
+
quietly(() => layOut(db, { fresh: true }));
|
|
69
|
+
const ztrack = source('ztrack.mjs'), opens = ztrack.indexOf('const BOARD_TABLES = `');
|
|
70
|
+
if (opens < 0) throw new Error('ztrack.mjs has no BOARD_TABLES');
|
|
71
|
+
db.exec(ztrack.slice(ztrack.indexOf('`', opens) + 1, ztrack.indexOf('`;', opens)));
|
|
72
|
+
const layout = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name").all()
|
|
73
|
+
.map(({ type, name, tbl_name: table, sql }) => [type, name, table, sql]);
|
|
74
|
+
const modules = MODULES.map((file) => [file, code((READS[file] ?? []).reduce(without, source(file)))]);
|
|
75
|
+
return createHash('sha256').update(JSON.stringify({ layout, modules })).digest('hex');
|
|
76
|
+
} finally { db.close(); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function storageContract() {
|
|
80
|
+
const storage = createHash('sha256');
|
|
81
|
+
for (const file of MODULES) storage.update(file).update(readFileSync(new URL(file, import.meta.url)));
|
|
82
|
+
let formatDigest = null;
|
|
83
|
+
try { formatDigest = format(); } catch (error) { process.stderr.write(`board storage contract: no format digest (${error.message}); the owner compares storage\n`); }
|
|
84
|
+
return { storage: storage.digest('hex'), format: formatDigest };
|
|
85
|
+
}
|
package/board/store.mjs
CHANGED
|
@@ -370,18 +370,6 @@ function layOutNumbers(db) {
|
|
|
370
370
|
} catch (error) { db.exec('ROLLBACK TO supercode_card_numbering; RELEASE supercode_card_numbering'); throw error; }
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/** Only cards are numbered (VO-3578 row 22): a task's row (supercode_cards.subtask_of) is numbered by the insert trigger
|
|
374
|
-
* like any row, so a load gives each task's number back unless its source named it (`named`, task ids). The sequence
|
|
375
|
-
* returns to the highest number held, never below `floor` (what the source had already handed out). */
|
|
376
|
-
export function unnumberTasks(db, named = new Set(), floor = 0) {
|
|
377
|
-
const taken = db.prepare(`SELECT n.task_id FROM ${CARD_NUMBERS} n JOIN supercode_cards c ON c.task_id = n.task_id WHERE c.subtask_of IS NOT NULL`).all()
|
|
378
|
-
.filter((row) => !named.has(row.task_id));
|
|
379
|
-
if (!taken.length) return;
|
|
380
|
-
const drop = db.prepare(`DELETE FROM ${CARD_NUMBERS} WHERE task_id = ?`);
|
|
381
|
-
for (const row of taken) drop.run(row.task_id);
|
|
382
|
-
db.prepare(`UPDATE sqlite_sequence SET seq = MAX(?, (SELECT COALESCE(MAX(number), 0) FROM ${CARD_NUMBERS})) WHERE name = ?`).run(Number(floor) || 0, CARD_NUMBERS);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
373
|
/** Raise the numbers' high-water mark to the highest number held, and to `floor`: AUTOINCREMENT hands out only numbers
|
|
386
374
|
* above it. For a store whose numbers were given or copied explicitly. */
|
|
387
375
|
export function raiseCardNumbers(db, floor = 0) {
|
package/board/ztrack.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
19
19
|
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { DatabaseSync } from 'node:sqlite';
|
|
22
|
-
import { actorOf, boardKey, CARD_NUMBERS, contention, raiseCardNumbers,
|
|
22
|
+
import { actorOf, boardKey, CARD_NUMBERS, contention, raiseCardNumbers, event, json, now, principalOf, storageFault, STORAGE_TRIES, unreadable } from './store.mjs';
|
|
23
23
|
import { GitDocument } from './git-document.mjs';
|
|
24
24
|
import { unimported } from './legacy-store.mjs';
|
|
25
25
|
import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
|
|
@@ -462,14 +462,13 @@ class ZtrackBoard {
|
|
|
462
462
|
put(`chunks:${role}`, entries);
|
|
463
463
|
}
|
|
464
464
|
put('published', published);
|
|
465
|
-
// The earlier store held no numbers: its cards take the numbers their documents' headings give them, and the
|
|
466
|
-
//
|
|
465
|
+
// The earlier store held no numbers: its cards take the numbers their documents' headings give them, and the rest
|
|
466
|
+
// are numbered in the order they were made, after every number the documents name (documentNumbers).
|
|
467
467
|
db.exec(`DELETE FROM ${CARD_NUMBERS}; DELETE FROM sqlite_sequence WHERE name = '${CARD_NUMBERS}'`);
|
|
468
468
|
const named = this.documentNumbers();
|
|
469
469
|
this.numberFromHeadings([...named.headings].filter(([id]) => db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(id)), db);
|
|
470
470
|
raiseCardNumbers(db, named.highest);
|
|
471
|
-
db.exec(`INSERT INTO ${CARD_NUMBERS} (task_id) SELECT id FROM tasks WHERE id NOT IN (SELECT task_id FROM ${CARD_NUMBERS})
|
|
472
|
-
AND id NOT IN (SELECT task_id FROM supercode_cards WHERE subtask_of IS NOT NULL) ORDER BY created_at, id`);
|
|
471
|
+
db.exec(`INSERT INTO ${CARD_NUMBERS} (task_id) SELECT id FROM tasks WHERE id NOT IN (SELECT task_id FROM ${CARD_NUMBERS}) ORDER BY created_at, id`);
|
|
473
472
|
db.exec('COMMIT');
|
|
474
473
|
}
|
|
475
474
|
|
|
@@ -922,10 +921,6 @@ class ZtrackBoard {
|
|
|
922
921
|
this.numberFromHeadings([...plan.numbers].filter(([id]) => plan.rows.has(id)));
|
|
923
922
|
raiseCardNumbers(this.conn, this.documentNumbers().highest);
|
|
924
923
|
}
|
|
925
|
-
// What is numbered before the cards load: a task the document or the store had numbered keeps it, and a task row the
|
|
926
|
-
// load inserts gives back the number the insert trigger hands it (only cards are numbered).
|
|
927
|
-
const numbered = overlay ? null : new Set(this.statement(`SELECT task_id FROM main.${CARD_NUMBERS}`).all().map((row) => row.task_id));
|
|
928
|
-
const handedOut = overlay ? 0 : this.one(`SELECT seq FROM main.sqlite_sequence WHERE name = '${CARD_NUMBERS}'`)?.seq ?? 0;
|
|
929
924
|
for (const [id, rows] of plan.rows) {
|
|
930
925
|
const before = fingerprintOf(id);
|
|
931
926
|
const oldTask = this.one(`SELECT * FROM ${schema}.tasks WHERE id = ?`, id) ?? {};
|
|
@@ -944,7 +939,6 @@ class ZtrackBoard {
|
|
|
944
939
|
events.push({ id, kind, payload: { by, source: 'document', status: after.status, assignee: after.assignee, author: by }, before, after });
|
|
945
940
|
}
|
|
946
941
|
}
|
|
947
|
-
if (!overlay) unnumberTasks(this.conn, numbered, handedOut);
|
|
948
942
|
for (const id of plan.checked) this.errors.delete(id);
|
|
949
943
|
for (const [id, reason] of plan.errors) this.errors.set(id, reason);
|
|
950
944
|
// A read overlays new document cards before the owner commits them. Give that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.225",
|
|
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": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"check-types": "node scripts/generate-types.mjs --check"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@volter/supercode-teams": "^0.3.
|
|
39
|
+
"@volter/supercode-teams": "^0.3.382",
|
|
40
40
|
"@volter/supercode-terminal": "^0.3.12",
|
|
41
41
|
"@volter/supercode-harness-sdk": "^0.3.106",
|
|
42
42
|
"@volter/teams": "0.2.106",
|