@volter/supercode-orchestrator 0.5.159 → 0.5.160
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 +24 -4
- package/board/dispatch.mjs +3 -6
- package/board/store.mjs +66 -15
- package/package.json +1 -1
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, 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';
|
|
29
|
+
import { actorOf, CALLER_RUN_ADDRESS, SESSION_CURRENT_RUNS, SESSION_RUNS, sessionIndexed, 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';
|
|
@@ -1406,18 +1406,38 @@ function workerRun(db, id, sessionId) {
|
|
|
1406
1406
|
* worked card is held to nothing here. The newest run alone named one card, and a session adopted into a second card
|
|
1407
1407
|
* was refused every write on its first (volter-org t_9a9c2a9c: no comment, heartbeat or closed subtask there, read
|
|
1408
1408
|
* by the board as no progress).
|
|
1409
|
+
*
|
|
1410
|
+
* Asked on every verb a session runs, so it reads the session's own runs and no others where it can (store.mjs). The
|
|
1411
|
+
* newest run naming the session is the newest whose text holds its id anywhere, an observation of another run's
|
|
1412
|
+
* included, which no index answers: it is looked for only when its answer can add a card (one of the session's own
|
|
1413
|
+
* runs is on a card it does not already work and is still the last worker of), and then from the newest run back to
|
|
1414
|
+
* the session's own newest, which its text names too. A session whose runs are on more cards than OWN_CARDS_ASKED is
|
|
1415
|
+
* not asked card by card first: the newest run is looked for and its card asked alone, which answers the same.
|
|
1416
|
+
* A board without the session's index (store.mjs sessionIndexed) is asked from its cards and by text alone, which
|
|
1417
|
+
* reads no run it cannot read.
|
|
1409
1418
|
*/
|
|
1419
|
+
const OWN_CARDS_ASKED = 8;
|
|
1410
1420
|
function ownCards(db, sessionId) {
|
|
1411
1421
|
const worked = new Set(), held = new Set();
|
|
1412
1422
|
const like = `%"session_id":"${sessionId}"%`;
|
|
1413
|
-
|
|
1423
|
+
const byIndex = sessionIndexed(db);
|
|
1424
|
+
for (const row of db.prepare(byIndex ? SESSION_CURRENT_RUNS : 'SELECT t.id, r.metadata FROM tasks t JOIN task_runs r ON r.id = t.current_run_id WHERE r.metadata LIKE ?').all(...(byIndex ? [sessionId, like] : [like]))) {
|
|
1414
1425
|
const session = json(row.metadata, {})?.supercode;
|
|
1415
1426
|
if (session?.session_id !== sessionId) continue;
|
|
1416
1427
|
held.add(row.id);
|
|
1417
1428
|
if ((session.slot ?? 'worker') === 'worker') worked.add(row.id);
|
|
1418
1429
|
}
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1430
|
+
const anywhere = 'SELECT task_id FROM task_runs WHERE metadata LIKE ? ORDER BY id DESC LIMIT 1';
|
|
1431
|
+
const asked = new Map();
|
|
1432
|
+
const lastWorker = (id) => { if (!asked.has(id)) asked.set(id, engine.lastSession(db, id)?.session_id === sessionId); return asked.get(id); };
|
|
1433
|
+
let newest;
|
|
1434
|
+
if (byIndex) {
|
|
1435
|
+
const runs = db.prepare(SESSION_RUNS).all(sessionId);
|
|
1436
|
+
const between = [...new Set(runs.map((run) => run.task_id))].filter((id) => !worked.has(id));
|
|
1437
|
+
if (!between.length || (between.length <= OWN_CARDS_ASKED && !between.some(lastWorker))) return { worked, held };
|
|
1438
|
+
newest = db.prepare('SELECT task_id FROM task_runs WHERE id >= ? AND metadata LIKE ? ORDER BY id DESC LIMIT 1').get(runs.at(-1).id, like) ?? db.prepare(anywhere).get(like);
|
|
1439
|
+
} else newest = db.prepare(anywhere).get(like);
|
|
1440
|
+
if (newest && lastWorker(newest.task_id)) { worked.add(newest.task_id); held.add(newest.task_id); }
|
|
1421
1441
|
return { worked, held };
|
|
1422
1442
|
}
|
|
1423
1443
|
|
package/board/dispatch.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { homedir, hostname, tmpdir, totalmem } from 'node:os';
|
|
|
40
40
|
import { basename, dirname, join } from 'node:path';
|
|
41
41
|
import { createHash, randomUUID } from 'node:crypto';
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
|
-
import { view, boardCopyOf, boardPath, briefPath, claimer, claimHolds, copyRefusal, event, json, listBoards, logsDir, newClaim, now, openBoard, setActor, storageFault, tx } from './store.mjs';
|
|
43
|
+
import { view, boardCopyOf, boardPath, briefPath, claimer, claimHolds, copyRefusal, event, json, listBoards, logsDir, newClaim, now, openBoard, pendingLaunches, sessionBound, setActor, storageFault, tx } from './store.mjs';
|
|
44
44
|
import { doors, fire, loadWorkflow, resumeOf, runSpec, runWorkingSeconds, requestSessionClose, retireSupersededRuns, refreshCardInputs, refreshRelationAnswers, transitionsFor, scopeOf, statusOf, projectStatus, storedStatus, templateValue, workflowFor } from './workflow.mjs';
|
|
45
45
|
import { checkWorkspace, assertWorkspaceOwner, notePresence, resolveWorkspace } from './workspace.mjs';
|
|
46
46
|
import { capacity, harnessDefaults, holdings } from './capacity.mjs';
|
|
@@ -1822,11 +1822,8 @@ function bindLaunch(board, row, session, pane, sid) {
|
|
|
1822
1822
|
/** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
|
|
1823
1823
|
export async function reconcileCallerLaunch(board, sid) {
|
|
1824
1824
|
fleetRoot = board.root;
|
|
1825
|
-
if (view(board, (db) => db.prepare(
|
|
1826
|
-
|
|
1827
|
-
const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
|
|
1828
|
-
JOIN tasks t ON t.current_run_id=r.id WHERE r.ended_at IS NULL
|
|
1829
|
-
AND json_extract(r.metadata, '$.supercode.launch_pending') = 1`).all());
|
|
1825
|
+
if (view(board, (db) => db.prepare(sessionBound(db)).get(sid))) return;
|
|
1826
|
+
const rows = view(board, (db) => db.prepare(pendingLaunches(db)).all());
|
|
1830
1827
|
const machine = localMachine();
|
|
1831
1828
|
const harness = boardCaller()?.harness ?? (sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code');
|
|
1832
1829
|
const candidates = rows.filter((row) => {
|
package/board/store.mjs
CHANGED
|
@@ -148,8 +148,54 @@ export function listBoards(root) {
|
|
|
148
148
|
// SQLite uses an expression index only for a statement that writes the expression the same way.
|
|
149
149
|
const RUN_SESSION = "json_extract(metadata, '$.supercode.session_id')";
|
|
150
150
|
const RUN_ADDRESS = "json_extract(metadata, '$.supercode.address')";
|
|
151
|
+
const RUN_LAUNCH_PENDING = "json_extract(metadata, '$.supercode.launch_pending')";
|
|
151
152
|
export const CALLER_RUN_ADDRESS = `SELECT ${RUN_ADDRESS} AS address FROM task_runs WHERE ${RUN_SESSION} = ? ORDER BY id DESC LIMIT 1`;
|
|
152
153
|
const RUN_SESSION_INDEX = 'idx_runs_supercode_session';
|
|
154
|
+
const RUN_LAUNCH_PENDING_INDEX = 'idx_runs_supercode_launch_pending';
|
|
155
|
+
// The indexes on a run's metadata. The second holds only the runs whose launch is pending (7 of 1,387 on the store
|
|
156
|
+
// these were measured for), in the runs' own order.
|
|
157
|
+
const RUN_INDEXES = [
|
|
158
|
+
[RUN_SESSION_INDEX, `(${RUN_SESSION}, id, ${RUN_ADDRESS})`],
|
|
159
|
+
[RUN_LAUNCH_PENDING_INDEX, `(id) WHERE ${RUN_LAUNCH_PENDING} = 1`],
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// What every verb a session runs asks about its caller (cli.mjs ownCards, dispatch.mjs reconcileCallerLaunch). Each
|
|
163
|
+
// starts from the runs an index names (the session's, or the pending launches) and reaches the card through the run's
|
|
164
|
+
// own `task_id`, the card's key: a card's current run is one of its own runs, and the join says both. Starting from the
|
|
165
|
+
// cards instead read every card's row to find the few that have a current run (810 of the 843 pages each of these
|
|
166
|
+
// statements read, whatever the session, on a store of 2,636 cards with 5 of them running). `r` is read from the session's
|
|
167
|
+
// index alone and the run's metadata only as `m`, after the card is known to name the run as current, so a session's
|
|
168
|
+
// earlier runs cost a page each and none of their metadata. The rows come in the cards' own order, as they did.
|
|
169
|
+
//
|
|
170
|
+
// Each of these is asked only of a board that has the index it starts from (sessionIndexed, sessionBound,
|
|
171
|
+
// pendingLaunches). Without the index it would read every run, and a board is without it exactly when one of its runs
|
|
172
|
+
// holds metadata SQLite cannot read as JSON (layOut), which `json_extract` refuses: the statement would fail on a run
|
|
173
|
+
// it had no reason to read. Such a board is asked the earlier statements instead, which start from the cards and read
|
|
174
|
+
// only their current runs. The board is asked which it has at each call, not told at its open: the owner holds a board
|
|
175
|
+
// open for hours, and the index may be made meanwhile by another process's open.
|
|
176
|
+
const ofRun = (expression, run) => expression.replace('(metadata,', `(${run}.metadata,`);
|
|
177
|
+
const CURRENT_RUN_OF = 'JOIN tasks t ON t.id = r.task_id AND t.current_run_id = r.id';
|
|
178
|
+
/** The cards whose current run names the session (`?`), with that run's metadata; `LIKE ?` as the run's text says it. */
|
|
179
|
+
export const SESSION_CURRENT_RUNS = `SELECT t.id, m.metadata FROM task_runs r ${CURRENT_RUN_OF} CROSS JOIN task_runs m ON m.id = r.id
|
|
180
|
+
WHERE ${ofRun(RUN_SESSION, 'r')} = ? AND m.metadata LIKE ? ORDER BY t.rowid`;
|
|
181
|
+
/** Every run that names the session (`?`) as its own, oldest first, with its card. */
|
|
182
|
+
export const SESSION_RUNS = `SELECT id, task_id FROM task_runs WHERE ${RUN_SESSION} = ? ORDER BY id`;
|
|
183
|
+
const SESSION_BOUND = `SELECT 1 FROM task_runs r ${CURRENT_RUN_OF} CROSS JOIN task_runs m ON m.id = r.id
|
|
184
|
+
WHERE ${ofRun(RUN_SESSION, 'r')} = ? AND COALESCE(${ofRun(RUN_LAUNCH_PENDING, 'm')},0)=0`;
|
|
185
|
+
// The runs come first (CROSS JOIN): the planner cannot know how few the index holds, and reads every card otherwise.
|
|
186
|
+
const PENDING_LAUNCHES = `SELECT r.id, r.task_id, r.metadata FROM task_runs r CROSS ${CURRENT_RUN_OF}
|
|
187
|
+
WHERE r.ended_at IS NULL AND ${ofRun(RUN_LAUNCH_PENDING, 'r')} = 1 ORDER BY t.rowid`;
|
|
188
|
+
// the earlier statements, from the cards: they read every card and the current runs alone
|
|
189
|
+
const CURRENT_RUN = 'FROM task_runs r JOIN tasks t ON t.current_run_id=r.id';
|
|
190
|
+
const SESSION_BOUND_BY_CARDS = `SELECT 1 ${CURRENT_RUN} WHERE ${ofRun(RUN_SESSION, 'r')}=? AND COALESCE(${ofRun(RUN_LAUNCH_PENDING, 'r')},0)=0`;
|
|
191
|
+
const PENDING_LAUNCHES_BY_CARDS = `SELECT r.id, r.task_id, r.metadata ${CURRENT_RUN} WHERE r.ended_at IS NULL AND ${ofRun(RUN_LAUNCH_PENDING, 'r')} = 1`;
|
|
192
|
+
const indexed = (db, index) => Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?").get(index));
|
|
193
|
+
/** Whether the board has the index SESSION_CURRENT_RUNS and SESSION_RUNS start from. */
|
|
194
|
+
export const sessionIndexed = (db) => indexed(db, RUN_SESSION_INDEX);
|
|
195
|
+
/** The statement that says whether a card's current run names the session (`?`) with its launch already bound. */
|
|
196
|
+
export const sessionBound = (db) => (sessionIndexed(db) ? SESSION_BOUND : SESSION_BOUND_BY_CARDS);
|
|
197
|
+
/** The statement that lists the open current runs whose launch is still pending. */
|
|
198
|
+
export const pendingLaunches = (db) => (indexed(db, RUN_LAUNCH_PENDING_INDEX) ? PENDING_LAUNCHES : PENDING_LAUNCHES_BY_CARDS);
|
|
153
199
|
|
|
154
200
|
/** Hermes's tables on a fresh board, supercode's own table and columns on every one. */
|
|
155
201
|
export function layOut(db, { fresh }) {
|
|
@@ -162,22 +208,27 @@ export function layOut(db, { fresh }) {
|
|
|
162
208
|
// each read walked every card's row. Laid out here, after the column it names exists on a store of any age and before
|
|
163
209
|
// any statement is prepared; no statement names it, so a store without it is only slower.
|
|
164
210
|
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)
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// metadata
|
|
211
|
+
// The calling session's run (CALLER_RUN_ADDRESS) and its cards (SESSION_CURRENT_RUNS, SESSION_BOUND, SESSION_RUNS):
|
|
212
|
+
// without the session's index each lookup read every run's whole metadata, overflow pages included. `id` keeps a
|
|
213
|
+
// session's runs in the order the lookups ask for, and the address makes the index answer the caller's lookup without
|
|
214
|
+
// reading the run. The pending launches (PENDING_LAUNCHES) have an index of their own, which holds only them. No
|
|
215
|
+
// statement names either, and a release that does not know one keeps it current like any other index. An index's
|
|
216
|
+
// first creation reads `task_runs` once.
|
|
217
|
+
// A run whose metadata SQLite cannot read as JSON refuses the creation: the board opens without the indexes, says so
|
|
218
|
+
// once with the rows to look for, and tries again on its next open. Without them the caller's lookup reads every run
|
|
219
|
+
// and fails at the run that refused them once it reaches it, and a session's cards are found the earlier way, from the
|
|
220
|
+
// cards (sessionIndexed). Once an index is there, SQLite refuses a write of such metadata to a run.
|
|
172
221
|
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'task_runs'").get()) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
222
|
+
const refused = [];
|
|
223
|
+
for (const [index, on] of RUN_INDEXES) {
|
|
224
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS ${index} ON task_runs${on}`); }
|
|
225
|
+
catch (error) { if (contention(error)) throw error; refused.push([index, error.message]); }
|
|
226
|
+
}
|
|
227
|
+
const said = current().said;
|
|
228
|
+
if (refused.length && !said.has(RUN_SESSION_INDEX)) {
|
|
229
|
+
said.add(RUN_SESSION_INDEX);
|
|
230
|
+
const names = refused.map(([index]) => index).join(' and ');
|
|
231
|
+
current().err(`board: warning: ${refused.length > 1 ? 'indexes' : 'index'} ${names} on task_runs ${refused.length > 1 ? 'were' : 'was'} not created (${refused[0][1]}); the board is open without ${refused.length > 1 ? 'them' : 'it'}: each session's verb reads every run for its caller, and finds its cards from the cards. The runs that refuse ${refused.length > 1 ? 'them' : 'it'}: SELECT id FROM task_runs WHERE metadata IS NOT NULL AND NOT json_valid(metadata, 6)\n`);
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
234
|
const effectColumns = new Set(db.prepare('PRAGMA table_info(supercode_effects)').all().map((c) => c.name));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.160",
|
|
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": {
|