@volter/supercode-orchestrator 0.5.15 → 0.5.16
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/capacity.mjs +7 -3
- package/board/dispatch.mjs +2 -2
- package/board/git-document.mjs +39 -8
- package/board/ztrack.mjs +10 -0
- package/package.json +1 -1
package/board/capacity.mjs
CHANGED
|
@@ -46,7 +46,7 @@ function sessionsOf(db, taskId) {
|
|
|
46
46
|
* own transcript reports it runs, or null where none can be read; `machineOf` normalises a machine name; `lanes` gives
|
|
47
47
|
* a profile's harness and model.
|
|
48
48
|
*/
|
|
49
|
-
export function holdings(board, { lanes, fleetOk, alive, liveRecord, modelOf = () => null, storedOf = null, machineOf }) {
|
|
49
|
+
export function holdings(board, { lanes, fleetOk, alive, liveRecord, modelOf = () => null, storedOf = null, machineOf, home = null }) {
|
|
50
50
|
const out = [];
|
|
51
51
|
const rows = board.db.prepare("SELECT t.id, t.status, t.assignee, t.current_run_id, t.model_override, c.machine, c.subtask_of FROM tasks t LEFT JOIN supercode_cards c ON c.task_id = t.id WHERE t.status != 'archived'").all();
|
|
52
52
|
for (const row of rows) {
|
|
@@ -56,7 +56,8 @@ export function holdings(board, { lanes, fleetOk, alive, liveRecord, modelOf = (
|
|
|
56
56
|
// what the board stores for an arc is read through its workflow's stored_as rules now, never the column a write has
|
|
57
57
|
// not yet brought up to date (a paused card counted as running until its row caught up)
|
|
58
58
|
stored: !row.subtask_of && storedOf ? storedOf(row.id) : row.status,
|
|
59
|
-
machine:
|
|
59
|
+
// a card that names no machine runs on this home's: a cap on this machine counts it
|
|
60
|
+
machine: row.machine ? machineOf(row.machine) : home,
|
|
60
61
|
model: row.model_override ?? lane.model ?? null, harness: lane.harness ?? null,
|
|
61
62
|
};
|
|
62
63
|
// a session's model: what its transcript reports, else what its launch resolved, else the card's
|
|
@@ -165,7 +166,10 @@ export function capacity(caps, held, evaluate) {
|
|
|
165
166
|
for (const counts of ['runs', 'cards', 'sessions', 'turns', 'starts']) {
|
|
166
167
|
const had = things.find((x) => x.card === entry.card && x.counts === counts);
|
|
167
168
|
// the card is counted already: it is now in the status its start moved it to
|
|
168
|
-
|
|
169
|
+
// the start is the card's next run, in its assignee's harness and model now (a card reassigned from Codex to
|
|
170
|
+
// Claude Code counts against Claude Code's caps from this start, not as the harness its last session ran)
|
|
171
|
+
if (had && counts === 'sessions' && had.harness !== entry.harness) things.push({ ...entry, counts });
|
|
172
|
+
else if (had) { if (counts !== 'sessions' && counts !== 'turns') Object.assign(had, { status: entry.status, stored: entry.stored, model: entry.model ?? had.model, harness: entry.harness ?? had.harness, machine: entry.machine ?? had.machine }); }
|
|
169
173
|
else things.push({ ...entry, counts });
|
|
170
174
|
}
|
|
171
175
|
},
|
package/board/dispatch.mjs
CHANGED
|
@@ -459,7 +459,7 @@ function homeHoldings(root, slugs, lanes, fleet, entry) {
|
|
|
459
459
|
try {
|
|
460
460
|
b.slug ??= slug; b.label ??= boardLabel(root, slug);
|
|
461
461
|
const storedOf = (id) => storedStatus(b.db, b, entry, id).stored;
|
|
462
|
-
out.push(...holdings(b, { lanes, fleetOk: fleet.ok, alive: (s) => sessionAlive(fleet, s), liveRecord: live, modelOf: transcriptModel, storedOf, machineOf: normalMachine }));
|
|
462
|
+
out.push(...holdings(b, { lanes, fleetOk: fleet.ok, alive: (s) => sessionAlive(fleet, s), liveRecord: live, modelOf: transcriptModel, storedOf, machineOf: normalMachine, home: localMachine() }));
|
|
463
463
|
}
|
|
464
464
|
finally { b.close(); }
|
|
465
465
|
} catch { /* its own tick reports it */ }
|
|
@@ -569,7 +569,7 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
569
569
|
});
|
|
570
570
|
// the caps, over what the home holds now; what each candidate would be once started
|
|
571
571
|
const caps = capacity(d.capacity, held, evaluate);
|
|
572
|
-
const entryOf = (c) => ({ card: c.task.id, board: board.slug, machine: c.task.machine
|
|
572
|
+
const entryOf = (c) => ({ card: c.task.id, board: board.slug, machine: normalMachine(c.task.machine ?? localMachine()), assignee: c.plan.profile ?? null,
|
|
573
573
|
status: c.plan.target, stored: c.plan.target, model: c.task.model_override ?? c.plan.lane?.model ?? held.defaults?.[c.plan.lane?.harness] ?? null, harness: c.plan.lane?.harness ?? null });
|
|
574
574
|
const waitingEntries = candidates.map(entryOf);
|
|
575
575
|
// a dry run (dispatch --dry-run) plans the same way and writes nothing: no notes, no moves, no launches
|
package/board/git-document.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// either side wrote is dropped; a merge that cannot be made stops the push and says why, leaving both texts in the
|
|
8
8
|
// checkout's history.
|
|
9
9
|
import { execFileSync, spawn } from 'node:child_process';
|
|
10
|
-
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { basename, dirname, relative } from 'node:path';
|
|
12
12
|
|
|
13
|
-
const git = (cwd, args, input) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], ...(input === undefined ? {} : { input }), timeout
|
|
14
|
-
const quiet = (cwd, args) => { try { return git(cwd, args); } catch { return null; } };
|
|
13
|
+
const git = (cwd, args, input, timeout = 120_000) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], ...(input === undefined ? {} : { input }), timeout });
|
|
14
|
+
const quiet = (cwd, args, timeout) => { try { return git(cwd, args, undefined, timeout); } catch { return null; } };
|
|
15
15
|
// Another board process (or the manager's own git) may hold git's index for a moment: its index.lock is waited out, not
|
|
16
16
|
// raced. The board keeps no lock of its own (ADR 0003).
|
|
17
17
|
const indexed = (cwd, args) => {
|
|
@@ -84,20 +84,51 @@ export class GitDocument {
|
|
|
84
84
|
// The push leaves the write's path: the commit is the write, and a push the remote refuses is taken up by the next
|
|
85
85
|
// write's pull (unpushed commits), which merges what was pushed and pushes before it writes.
|
|
86
86
|
// While it runs, this process's next write does not push the same commits beside it.
|
|
87
|
-
|
|
88
|
-
this.pushing =
|
|
89
|
-
|
|
87
|
+
// Writes that land while it runs are one more push after it (a burst publishes in two pushes, not one per write).
|
|
88
|
+
if (this.pushing) { this.again = true; return; }
|
|
89
|
+
this.pushInBackground(branch);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `git push` beside the process, outliving it. Its refusal is kept where `behind()` reads it, and cleared by a push
|
|
93
|
+
* that lands. */
|
|
94
|
+
pushInBackground(branch) {
|
|
95
|
+
const failure = this.failurePath();
|
|
96
|
+
const pushing = spawn('sh', ['-c', 'git push --quiet origin "HEAD:$1" 2>"$2.tmp" && rm -f "$2" "$2.tmp" || mv -f "$2.tmp" "$2"', 'push', branch, failure], { cwd: this.root, stdio: 'ignore', detached: true });
|
|
97
|
+
this.pushing = pushing; this.again = false;
|
|
98
|
+
const done = () => {
|
|
99
|
+
if (this.pushing !== pushing) return;
|
|
100
|
+
this.pushing = null;
|
|
101
|
+
if (this.again) this.pushInBackground(branch);
|
|
102
|
+
};
|
|
90
103
|
pushing.on('error', done); pushing.on('exit', done);
|
|
91
104
|
pushing.unref();
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
failurePath() {
|
|
108
|
+
return git(this.root, ['rev-parse', '--git-path', 'supercode-board-push.err']).trim().replace(/^(?!\/)/, `${this.root}/`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The board documents' publish when it is behind: commits the remote lacks, the oldest one's time and the last push's
|
|
112
|
+
* error; null when the remote has everything (or this checkout pushes nowhere). Local reads only, no network. */
|
|
113
|
+
behind() {
|
|
114
|
+
if (!this.usable) return null;
|
|
115
|
+
const branch = this.branch();
|
|
116
|
+
if (!branch) return null;
|
|
117
|
+
const times = (quiet(this.root, ['log', '--format=%ct', `origin/${branch}..HEAD`]) ?? '').trim().split('\n').filter(Boolean).map(Number);
|
|
118
|
+
if (!times.length) return null;
|
|
119
|
+
let error = null;
|
|
120
|
+
try { error = readFileSync(this.failurePath(), 'utf8').split('\n').map((line) => line.replace(/\s+/g, ' ').trim()).find(Boolean) ?? null; } catch { /* none recorded */ }
|
|
121
|
+
return { commits: times.length, since: Math.min(...times) * 1000, error };
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
/** The board's round: publish what an earlier write committed and the remote has not taken (a refused or failed
|
|
95
125
|
* background push), with the same merge and three attempts a write's pull uses. */
|
|
96
126
|
publishPending() {
|
|
97
127
|
if (!this.usable || this.pushing) return;
|
|
98
128
|
const branch = this.branch();
|
|
99
129
|
if (!branch || !this.unpushed(branch)) return;
|
|
100
|
-
|
|
130
|
+
// A remote that hangs costs the round 15 s, not git's two minutes.
|
|
131
|
+
if (quiet(this.root, ['fetch', '--quiet', 'origin', branch], 15_000) === null) return; // offline: the next round tries again
|
|
101
132
|
if (this.unpushed(branch)) this.publish(branch, 'board');
|
|
102
133
|
}
|
|
103
134
|
|
|
@@ -109,7 +140,7 @@ export class GitDocument {
|
|
|
109
140
|
/** Push what this checkout holds, taking anything pushed meanwhile. */
|
|
110
141
|
publish(branch, who) {
|
|
111
142
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
112
|
-
if (quiet(this.root, ['push', '--quiet', 'origin', `HEAD:${branch}`]) !== null) return;
|
|
143
|
+
if (quiet(this.root, ['push', '--quiet', 'origin', `HEAD:${branch}`]) !== null) { try { rmSync(this.failurePath(), { force: true }); } catch { /* none */ } return; }
|
|
113
144
|
if (quiet(this.root, ['fetch', '--quiet', 'origin', branch]) === null) throw new Error('the board documents were committed here but could not be pushed: the remote did not answer');
|
|
114
145
|
this.integrate(branch, who);
|
|
115
146
|
}
|
package/board/ztrack.mjs
CHANGED
|
@@ -72,6 +72,8 @@ export function openZtrackBoard(marker, { create = false, document = null, seed
|
|
|
72
72
|
return { db: backing.db, tx: (fn) => backing.tx(fn), publishPending: () => backing.git?.publishPending(), close: () => backing.close(), document: backing.path };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const behindSaid = new Set();
|
|
76
|
+
|
|
75
77
|
class ZtrackBoard {
|
|
76
78
|
constructor({ document, seed, marker, init, root, slug, compatPath, commit = false, manager = null }) {
|
|
77
79
|
this.path = resolve(dirname(marker), document);
|
|
@@ -83,6 +85,14 @@ class ZtrackBoard {
|
|
|
83
85
|
// own work; the backing commits only the writes the manager does not make.
|
|
84
86
|
this.manager = manager;
|
|
85
87
|
this.git = commit ? new GitDocument([this.path, this.archive], (base, theirs, ours, path) => { try { return this.codec.mergeBoardDocument(base, theirs, ours, path)?.text ?? null; } catch { return null; } }) : null;
|
|
88
|
+
// A publish that is behind is said once per process, on whichever workflow verb opened the board: a refused push
|
|
89
|
+
// surfaces on the next call instead of only in the dispatcher's report, and stops being said once the push lands.
|
|
90
|
+
if (this.git && !behindSaid.has(this.path)) {
|
|
91
|
+
behindSaid.add(this.path);
|
|
92
|
+
const late = this.git.behind();
|
|
93
|
+
// A push younger than a minute is still on its way.
|
|
94
|
+
if (late && Date.now() - late.since > 60_000) process.stderr.write(`board: warning: ${late.commits} board document commit${late.commits === 1 ? '' : 's'} not published since ${new Date(late.since).toISOString().slice(0, 16)}Z${late.error ? ` (last push: ${late.error})` : ''}; the dispatcher's next round retries\n`);
|
|
95
|
+
}
|
|
86
96
|
this.seed = seed; this.root = root; this.slug = slug; this.init = init; this.compatPath = compatPath;
|
|
87
97
|
this.file = `${marker}.board.db`;
|
|
88
98
|
mkdirSync(dirname(this.path), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
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": {
|