@volter/supercode-orchestrator 0.4.91 → 0.4.93
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 +5 -5
- package/board/dispatch.mjs +16 -16
- package/board/engine.mjs +7 -7
- package/board/store.mjs +12 -17
- package/board/workflow.mjs +3 -1
- package/board/ztrack.mjs +2 -2
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import { basename, join, resolve } from 'node:path';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { read } from '../orchestration.mjs';
|
|
12
|
-
import { backingPaths, layOut, backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
12
|
+
import { view, backingPaths, layOut, backingOf, boardExists, boardPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
13
13
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
14
14
|
import * as engine from './engine.mjs';
|
|
15
15
|
import { adoptSession, boardAddress, boardLabel, boardSettings, callerSession, lanesOf, localMachine, observeFleet, paused, plan, startCard, tick } from './dispatch.mjs';
|
|
@@ -304,7 +304,7 @@ export async function runWorkflow(argv) {
|
|
|
304
304
|
// the creator hears what happens from here on; a start answered above is not told twice
|
|
305
305
|
const address = args['no-notify'] ? null : args.notify ?? callerAddress();
|
|
306
306
|
if (address) withBoard((b) => engine.subscribe(b, created.id, address, 'creator'));
|
|
307
|
-
const task = withBoard((b) =>
|
|
307
|
+
const task = withBoard((b) => view(b, (db) => engine.getTask(db, created.id)));
|
|
308
308
|
if (args.json) { say(args, '', { ...cardDict(task), ...answer, notify: address ?? null }); return answer.failed ? 1 : 0; }
|
|
309
309
|
const said = answer.started ? `; started session ${answer.session} (${answer.mode})` : answer.failed ? `; did not start: ${answer.failed}` : answer.waits ? `; waits: ${answer.waits}` : '';
|
|
310
310
|
say(args, `${created.existing ? 'Existing' : 'Created'} ${created.id} (${task.status}, assignee=${task.assignee || '-'})${said}${address ? `; notices go to ${address}` : ''}`, null);
|
|
@@ -435,7 +435,7 @@ export async function runWorkflow(argv) {
|
|
|
435
435
|
return 0;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
case 'context': need(1, 'a card id'); process.stdout.write(`${withBoard((b) =>
|
|
438
|
+
case 'context': need(1, 'a card id'); process.stdout.write(`${withBoard((b) => view(b, (db) => engine.cardContext(db, id)))}\n`); return 0;
|
|
439
439
|
|
|
440
440
|
case 'runs': {
|
|
441
441
|
need(1, 'a card id');
|
|
@@ -471,7 +471,7 @@ export async function runWorkflow(argv) {
|
|
|
471
471
|
const ttl = args.ttl ? duration(args.ttl) : engine.claimTtl();
|
|
472
472
|
const runId = withBoard((b) => engine.claim(b, id, { ttl }));
|
|
473
473
|
if (!runId) throw new Error(`cannot claim ${id} (not ready, already claimed, or waiting on parents)`);
|
|
474
|
-
const task = withBoard((b) =>
|
|
474
|
+
const task = withBoard((b) => view(b, (db) => engine.getTask(db, id)));
|
|
475
475
|
// Hermes's claim resolves the workspace and prints it; a scratch one is made under the board's workspaces
|
|
476
476
|
let workspace = task.workspace_path;
|
|
477
477
|
if (!workspace && task.workspace_kind === 'scratch') {
|
|
@@ -658,7 +658,7 @@ export async function runWorkflow(argv) {
|
|
|
658
658
|
const file = join(logsDir(root, slug), `${id}.log`);
|
|
659
659
|
if (!existsSync(file)) {
|
|
660
660
|
// a pane session keeps no log file: its transcript is the harness's own, named by the run
|
|
661
|
-
const s = withBoard((b) =>
|
|
661
|
+
const s = withBoard((b) => view(b, (db) => engine.lastSession(db, id)));
|
|
662
662
|
process.stdout.write(s ? `(${id} runs as session ${s.address}; its transcript is that session's: \`supercode sessions show ${s.session_id}\`)\n` : `(no log for ${id})\n`);
|
|
663
663
|
return 0;
|
|
664
664
|
}
|
package/board/dispatch.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { hostname, tmpdir } from 'node:os';
|
|
|
21
21
|
import { basename, dirname, join } from 'node:path';
|
|
22
22
|
import { randomUUID } from 'node:crypto';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
-
import { boardPath, claimer, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
24
|
+
import { view, boardPath, claimer, event, json, listBoards, logsDir, now, openBoard, setActor, tx } from './store.mjs';
|
|
25
25
|
import { doors, fire, loadWorkflow, resumeOf, runSpec, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, templateValue, workflowFor } from './workflow.mjs';
|
|
26
26
|
import { evaluate, holds, render } from './expr.mjs';
|
|
27
27
|
import {
|
|
@@ -417,22 +417,22 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
417
417
|
/** Launch what the card's status runs: its role's lane, in the run's session slot, told the run's prompt and doors. */
|
|
418
418
|
export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
419
419
|
const task = getTaskNow(board, id);
|
|
420
|
-
const status =
|
|
420
|
+
const status = view(board, (db) => statusOf(db, task));
|
|
421
421
|
const spec = runSpec(entry, status, task);
|
|
422
422
|
if (!spec) return null;
|
|
423
423
|
const runId = task.current_run_id;
|
|
424
424
|
const surface = lane.surface === 'pane' ? 'pane' : 'headless';
|
|
425
|
-
const scope =
|
|
425
|
+
const scope = view(board, (db) => scopeOf(db, board, task, { params: entry.params }));
|
|
426
426
|
const where = whereOf(board, task);
|
|
427
427
|
// a subtask's run also sees its arc: the arc's text, tasks and thread (where its owner's rulings are)
|
|
428
|
-
const arcContext = task.subtask_of ?
|
|
429
|
-
const prompt = render(spec.prompt, { ...scope, where, context:
|
|
428
|
+
const arcContext = task.subtask_of ? view(board, (db) => cardContext(db, task.subtask_of)) : '';
|
|
429
|
+
const prompt = render(spec.prompt, { ...scope, where, context: view(board, (db) => cardContext(db, id)), arc_context: arcContext, doors: doors(entry.workflow, status, spec.role, id, where, surface) });
|
|
430
430
|
// the run's session record knows its status, role and slot (a reviewer's slot is never the implementer's); the slot
|
|
431
431
|
// is a template, so a step can ask for isolation by naming a new one
|
|
432
432
|
const slot = String(templateValue(spec.session ?? 'worker', scope) ?? 'worker');
|
|
433
433
|
if (spec.role === 'reviewer') {
|
|
434
|
-
const worker =
|
|
435
|
-
const reviewer =
|
|
434
|
+
const worker = view(board, (db) => lastSession(db, id, 'worker'));
|
|
435
|
+
const reviewer = view(board, (db) => lastSession(db, id, slot));
|
|
436
436
|
if (slot === 'worker' || (worker?.session_id && worker.session_id === reviewer?.session_id)) throw new Error('a reviewer must have a different session from the implementer');
|
|
437
437
|
}
|
|
438
438
|
const meta = { status, role: spec.role, slot };
|
|
@@ -448,7 +448,7 @@ export async function startCard(board, id, { roles = ['manager'], lanes, setting
|
|
|
448
448
|
const moved = tx(board, (db) => fire(db, board, entry, id, entry.workflow.dispatch.start, { roles, payload: { lock: claimer() } }));
|
|
449
449
|
const task = getTaskNow(board, id);
|
|
450
450
|
const spec = runSpec(entry, moved.to, task);
|
|
451
|
-
const profile = spec ?
|
|
451
|
+
const profile = spec ? view(board, (db) => evaluate(entry.workflow.roles?.[spec.role] ?? 'card.assignee', scopeOf(db, board, task, { params: entry.params }))) : null;
|
|
452
452
|
const lane = profile ? lanes[profile] : null;
|
|
453
453
|
if (spec && !lane) throw new Error(`${id} is ${moved.to}, run by ${spec.role} (${profile ?? 'nobody'}), which is no lane of this home`);
|
|
454
454
|
return lane ? launchRun(board, entry, id, lane, fleet, settings) : null;
|
|
@@ -507,7 +507,7 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
|
|
|
507
507
|
/** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
|
|
508
508
|
export async function sessionEffect(board, e) {
|
|
509
509
|
if (e.kind === 'send_to_actor') {
|
|
510
|
-
const run =
|
|
510
|
+
const run = view(board, (db) => currentRun(db, e.id));
|
|
511
511
|
const session = run?.metadata?.supercode;
|
|
512
512
|
const address = addressOf(session);
|
|
513
513
|
if (!address) return;
|
|
@@ -515,7 +515,7 @@ export async function sessionEffect(board, e) {
|
|
|
515
515
|
return;
|
|
516
516
|
}
|
|
517
517
|
if (e.kind === 'close_session') {
|
|
518
|
-
const rows =
|
|
518
|
+
const rows = view(board, (db) => db.prepare('SELECT id, metadata FROM task_runs WHERE task_id = ? ORDER BY id DESC').all(e.id));
|
|
519
519
|
for (const row of rows) {
|
|
520
520
|
// a run newer than the one that left the status is its successor, already started in this tick: not closed
|
|
521
521
|
if (e.runId && row.id > e.runId) continue;
|
|
@@ -609,7 +609,7 @@ export function paused(root) {
|
|
|
609
609
|
|
|
610
610
|
|
|
611
611
|
|
|
612
|
-
const getTaskNow = (board, id) =>
|
|
612
|
+
const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
|
|
613
613
|
|
|
614
614
|
|
|
615
615
|
function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'worker', meta = {} } = {}) {
|
|
@@ -628,11 +628,11 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
|
|
|
628
628
|
// the slot's last session: kept when live, resumed when lost, never another slot's (a reviewer's is its own)
|
|
629
629
|
// a close its session's own command asked for is carried out first, so its pane is gone before the slot's next
|
|
630
630
|
// session starts (never two processes on one session)
|
|
631
|
-
let prev =
|
|
631
|
+
let prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
632
632
|
if (prev?.close_requested && !prev.closed_at) {
|
|
633
633
|
const report = { undelivered: [] };
|
|
634
634
|
closeRequested(board, report);
|
|
635
|
-
prev =
|
|
635
|
+
prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
636
636
|
// a pane that could not be closed still holds the session: no second process is started on it
|
|
637
637
|
if (prev?.close_requested && !prev.closed_at) throw new Error(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
|
|
638
638
|
}
|
|
@@ -867,7 +867,7 @@ async function send(from, label, to, body, key, { queue = false, ask = false } =
|
|
|
867
867
|
* (the id is the send's idempotency key). One that fails is tried again next tick.
|
|
868
868
|
*/
|
|
869
869
|
async function sendBoardMessages(board, report) {
|
|
870
|
-
const rows =
|
|
870
|
+
const rows = view(board, (db) => db.prepare(`SELECT id, task_id, payload FROM task_events e WHERE kind = 'board_message' AND NOT EXISTS
|
|
871
871
|
(SELECT 1 FROM task_events s WHERE s.kind = 'board_message_sent' AND s.task_id = e.task_id AND json_extract(s.payload, '$.event') = e.id) ORDER BY id`).all());
|
|
872
872
|
for (const row of rows) {
|
|
873
873
|
const m = json(row.payload, {}) ?? {};
|
|
@@ -937,7 +937,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
937
937
|
.all(sub.task_id, sub.last_event_id, ...kinds);
|
|
938
938
|
if (!events.length) return;
|
|
939
939
|
// the card as the workflow names it (its status the workflow's: triaging, reviewing, stopped; `stored_as` kanban.db's)
|
|
940
|
-
const task =
|
|
940
|
+
const task = view(board, (db) => { const t = getTask(db, sub.task_id); return t ? { ...t, status: statusOf(db, t), stored_as: t.status } : t; });
|
|
941
941
|
let cursor = Number(sub.last_event_id);
|
|
942
942
|
let failed = null;
|
|
943
943
|
try {
|
|
@@ -995,7 +995,7 @@ export async function notifyChats(root, adapters, log = () => {}, wake = null) {
|
|
|
995
995
|
if (sub.platform === 'supercode' || !adapter) continue;
|
|
996
996
|
const events = board.db.prepare(`SELECT id, kind, payload FROM task_events WHERE task_id = ? AND id > ? AND kind IN (${kinds.map(() => '?').join(', ')}) ORDER BY id`).all(sub.task_id, sub.last_event_id, ...kinds);
|
|
997
997
|
if (!events.length) continue;
|
|
998
|
-
const task =
|
|
998
|
+
const task = view(board, (db) => getTask(db, sub.task_id));
|
|
999
999
|
let cursor = Number(sub.last_event_id);
|
|
1000
1000
|
let failed = false;
|
|
1001
1001
|
for (const ev of events) {
|
package/board/engine.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { existsSync, rmSync } from 'node:fs';
|
|
12
12
|
import { join, resolve } from 'node:path';
|
|
13
13
|
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
-
import { claimer, event, json, newTaskId, now, tx } from './store.mjs';
|
|
14
|
+
import { view, claimer, event, json, newTaskId, now, tx } from './store.mjs';
|
|
15
15
|
import { refreshCardInputs, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
|
|
16
16
|
|
|
17
17
|
export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
|
|
@@ -80,7 +80,7 @@ export function endRun(db, taskId, { outcome, status = null, summary = null, err
|
|
|
80
80
|
// terminates a reclaimed worker (a board's one-shot workers run where the board is); a pane session is a person-visible session and is never killed from the board
|
|
81
81
|
const worker = outcome === 'reclaimed' ? json(was, {})?.supercode : null;
|
|
82
82
|
const lock = one(db, 'SELECT claim_lock FROM task_runs WHERE id = ?', runId)?.claim_lock ?? one(db, 'SELECT claim_lock FROM tasks WHERE id = ?', taskId)?.claim_lock;
|
|
83
|
-
if (worker?.surface === 'headless' && worker.pid && String(lock ?? '').startsWith(`${claimer().split(':')[0]}:`))
|
|
83
|
+
if (worker?.surface === 'headless' && worker.pid && String(lock ?? '').startsWith(`${claimer().split(':')[0]}:`)) run(db, 'INSERT INTO supercode_effects(effect) VALUES(?)', JSON.stringify({kind:'stop_run',id:taskId,runId}));
|
|
84
84
|
run(db, `UPDATE task_runs SET status = ?, outcome = ?, summary = ?, error = ?, metadata = ?, ended_at = ?, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL
|
|
85
85
|
WHERE id = ? AND ended_at IS NULL`, status ?? outcome, outcome, summary, error, keep, now(), runId);
|
|
86
86
|
run(db, 'UPDATE tasks SET current_run_id = NULL WHERE id = ?', taskId);
|
|
@@ -285,9 +285,9 @@ export function extendClaim(db, id, { ttl, reason, extra = {} }) {
|
|
|
285
285
|
* workspaces, or a worktree (its branch stays); never while a child still waits on it, never a `dir`.
|
|
286
286
|
*/
|
|
287
287
|
function cleanupWorkspace(board, id) {
|
|
288
|
-
const task =
|
|
288
|
+
const task = view(board, (db) => getTask(db, id));
|
|
289
289
|
if (!task?.workspace_path || !['scratch', 'worktree'].includes(task.workspace_kind)) return;
|
|
290
|
-
const waiting =
|
|
290
|
+
const waiting = view(board, (db) => one(db, "SELECT 1 FROM task_links l JOIN tasks t ON t.id = l.child_id WHERE l.parent_id = ? AND t.status NOT IN ('done', 'archived') LIMIT 1", id));
|
|
291
291
|
if (waiting || !existsSync(task.workspace_path)) return;
|
|
292
292
|
if (task.workspace_kind === 'worktree') { spawnSync('git', ['-C', task.workspace_path, 'worktree', 'remove', '--force', task.workspace_path], { encoding: 'utf8' }); return; }
|
|
293
293
|
const managed = resolve(board.slug === 'default' ? join(board.root, 'kanban', 'workspaces') : join(board.root, 'kanban', 'boards', board.slug, 'workspaces'));
|
|
@@ -368,7 +368,7 @@ export async function runEffects(board, effects = []) {
|
|
|
368
368
|
// a person-visible session and is never killed from the board
|
|
369
369
|
function stopWorker(board, runId) {
|
|
370
370
|
if (!runId) return;
|
|
371
|
-
const row =
|
|
371
|
+
const row = view(board, (db) => one(db, 'SELECT metadata, claim_lock FROM task_runs WHERE id = ?', runId));
|
|
372
372
|
const worker = json(row?.metadata, {})?.supercode;
|
|
373
373
|
if (worker?.surface === 'headless' && worker.pid) terminate(Number(worker.pid));
|
|
374
374
|
}
|
|
@@ -443,7 +443,7 @@ export const assign = (board, id, profile, { roles } = {}) => act(board, id, 'as
|
|
|
443
443
|
/** A triage card fans out into children it waits on (Hermes's decomposer); answers their ids. */
|
|
444
444
|
export async function decompose(board, id, children, { author = 'user', roles } = {}) {
|
|
445
445
|
if (!children.length) throw new Error('decompose names at least one child (--child "<title>[@<assignee>]")');
|
|
446
|
-
const task =
|
|
446
|
+
const task = view(board, (db) => must(db, id));
|
|
447
447
|
const ids = children.map((c) => createCard(board, {
|
|
448
448
|
title: c.title, body: c.body ?? null, assignee: c.assignee ?? task.assignee, machine: task.machine,
|
|
449
449
|
workspace: task.workspace_path ? `${task.workspace_kind}:${task.workspace_path}` : task.workspace_kind, tenant: task.tenant, createdBy: author,
|
|
@@ -464,7 +464,7 @@ export function link(board, parent, child, { roles } = {}) {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
export async function unlink(board, parent, child, { roles } = {}) {
|
|
467
|
-
const linked =
|
|
467
|
+
const linked = view(board, (db) => one(db, 'SELECT 1 FROM task_links WHERE parent_id = ? AND child_id = ?', parent, child));
|
|
468
468
|
if (!linked) return false;
|
|
469
469
|
await act(board, child, 'unlink', { roles, payload: { parent, child } });
|
|
470
470
|
return true;
|
package/board/store.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// This module opens a board in its backing and gives its callers one primitive, a write transaction; the transitions
|
|
10
10
|
// are in `engine.mjs`. Hermes's own tools may write a `kanban.db` at the same time: every write there takes SQLite's
|
|
11
|
-
//
|
|
11
|
+
// native transaction boundary and retries an optimistic conflict.
|
|
12
12
|
import { randomBytes } from 'node:crypto';
|
|
13
13
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { hostname } from 'node:os';
|
|
@@ -152,7 +152,6 @@ export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing
|
|
|
152
152
|
if (use === 'files') return { ...openFilesBoard(path, { create, init: layOut }), root, slug, path, backing: use };
|
|
153
153
|
if (!kept) mkdirSync(dirname(path), { recursive: true });
|
|
154
154
|
const db = new DatabaseSync(path);
|
|
155
|
-
db.exec('PRAGMA busy_timeout = 10000');
|
|
156
155
|
db.exec('PRAGMA foreign_keys = OFF');
|
|
157
156
|
const fresh = !db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tasks'").get();
|
|
158
157
|
if (fresh && !create) { db.close(); throw new Error(`${path} is not a board`); }
|
|
@@ -165,26 +164,22 @@ export function openBoard(root, slug = DEFAULT_BOARD, { create = false, backing
|
|
|
165
164
|
* A read inside the transaction is the state it writes over.
|
|
166
165
|
*/
|
|
167
166
|
export const tx = (board, fn) => board.tx(fn);
|
|
167
|
+
export const view = (board, fn) => fn(board.db);
|
|
168
168
|
|
|
169
|
-
//
|
|
169
|
+
// A deferred native transaction, with the whole operation retried on an optimistic conflict.
|
|
170
|
+
// There is no separate application writer lock or contention timeout.
|
|
170
171
|
function sqliteTx(db, fn) {
|
|
171
|
-
for (
|
|
172
|
+
for (;;) {
|
|
172
173
|
try {
|
|
173
|
-
db.exec('BEGIN
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
db.exec('BEGIN');
|
|
175
|
+
const out=fn(db);
|
|
176
|
+
db.exec('COMMIT');
|
|
177
|
+
return out;
|
|
178
|
+
} catch(error) {
|
|
179
|
+
try {db.exec('ROLLBACK');} catch {}
|
|
180
|
+
if (!/locked|busy/i.test(error.message))throw error;
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
|
-
try {
|
|
181
|
-
const out = fn(db);
|
|
182
|
-
db.exec('COMMIT');
|
|
183
|
-
return out;
|
|
184
|
-
} catch (error) {
|
|
185
|
-
try { db.exec('ROLLBACK'); } catch { /* already rolled back */ }
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
export const now = () => Math.floor(Date.now() / 1000);
|
package/board/workflow.mjs
CHANGED
|
@@ -116,7 +116,9 @@ export function refreshCardInputs(board, id, { commits = false } = {}) {
|
|
|
116
116
|
if (commits) {
|
|
117
117
|
const own=task.workspace_kind!=='scratch'?task.workspace_path:null;
|
|
118
118
|
const parent=!own && task.subtask_of?board.db.prepare('SELECT workspace_path FROM tasks WHERE id=?').get(task.subtask_of)?.workspace_path:null;
|
|
119
|
-
|
|
119
|
+
const path=own??parent??null;
|
|
120
|
+
if(path)commitTimes.delete(`${path}:${id}`);
|
|
121
|
+
observeLastCommit(path,id);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
package/board/ztrack.mjs
CHANGED
|
@@ -39,7 +39,7 @@ class ZtrackBoard {
|
|
|
39
39
|
this.codec = require('@volter/ztrack/board-document');
|
|
40
40
|
this.preset = require('@volter/ztrack/kanban-preset');
|
|
41
41
|
this.runtime = new DatabaseSync(`${marker}.runtime.db`);
|
|
42
|
-
if (!this.runtime.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='state'").get()) this.runtime.exec('CREATE TABLE state (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
|
42
|
+
this.readOnly(()=>{ if (!this.runtime.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='state'").get()) this.runtime.exec('CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)'); });
|
|
43
43
|
this.errors = new Map(); this.preserve = new Map();
|
|
44
44
|
this.mem = null; this.active = false; this.rows = new RuntimeRows(this.runtime);
|
|
45
45
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
@@ -169,7 +169,7 @@ class ZtrackBoard {
|
|
|
169
169
|
try { this.runtime.exec('ROLLBACK'); } catch { /* intent already durable */ }
|
|
170
170
|
this.invalidate();
|
|
171
171
|
if (/busy|locked|Hermes wrote|document changed during recovery/i.test(error.message)) continue;
|
|
172
|
-
if (committed) error.message += ' (the
|
|
172
|
+
if (committed) error.message += ' (the operation reached its durable intent; inspect its recorded outcome before retrying)';
|
|
173
173
|
throw error;
|
|
174
174
|
} finally { this.active = false; this.computing = false; this.pendingValues = null; }
|
|
175
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.93",
|
|
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": {
|