@volter/supercode-orchestrator 0.5.85 → 0.5.87
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 +7 -1
- package/board/dispatch.mjs +4 -2
- package/board/git-document.mjs +45 -10
- package/board/runtime-door.mjs +8 -3
- package/package.json +2 -2
package/board/cli.mjs
CHANGED
|
@@ -970,6 +970,11 @@ export async function runVerb(argv) {
|
|
|
970
970
|
say(args, lines.length ? lines.join('\n') : 'nothing would start', args.json ? { would, would_review: reviews } : would);
|
|
971
971
|
return 0;
|
|
972
972
|
}
|
|
973
|
+
// This process holds the home's lock for the round, so it is the board's owner for it: it registers its pid, which
|
|
974
|
+
// the daemon takes only because that pid holds the lock, so the card panes it launches as the board are the
|
|
975
|
+
// board's (SUPERCODE_BOARD_OWNER_PID, commandEnv). A later owner's registration replaces it.
|
|
976
|
+
try { await machineCall(null, 'harness.v1.boards.register', { root, claim: { pid: process.pid, ppid: process.ppid } }, { timeoutMs: 30_000 }); }
|
|
977
|
+
catch (error) { current().err(`supercode workflow: the machine daemon did not register this round as the board's owner (${error.message}); card panes it launches may not be the board's\n`); }
|
|
973
978
|
const reports = await tick({ root, lanes, settings, params });
|
|
974
979
|
if (args.json) say(args, '', reports);
|
|
975
980
|
else for (const r of reports) current().out(`${summary(r)}\n`);
|
|
@@ -1678,7 +1683,8 @@ async function serve(root, args = {}) {
|
|
|
1678
1683
|
// The board's events are published by this machine's connector, not by the dispatcher (D137, D138 step 4): the home is
|
|
1679
1684
|
// named to the daemon once, and the connector follows its `workflow events` from then on, with or without this serve.
|
|
1680
1685
|
{ let named = null, refusal = null;
|
|
1681
|
-
|
|
1686
|
+
// this process is the home's owner: it names its pid (its claim), which the daemon takes only if it holds the home's lock
|
|
1687
|
+
try { named = await machineCall(null, 'harness.v1.boards.register', { root, claim: { pid: process.pid, ppid: process.ppid } }, { timeoutMs: 30_000 }); } catch (error) { refusal = error.message; }
|
|
1682
1688
|
const refused = named?.refused ?? null;
|
|
1683
1689
|
log(refusal ? `board events: not published; the machine daemon did not take this home: ${refusal}`
|
|
1684
1690
|
: refused ? `board events: registered, but the team refused this machine's credential (${refused.status}); nothing is published until it is re-enrolled`
|
package/board/dispatch.mjs
CHANGED
|
@@ -76,9 +76,11 @@ export function localMachine() {
|
|
|
76
76
|
/** The supercode command the dispatcher drives. */
|
|
77
77
|
export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
|
|
78
78
|
|
|
79
|
-
/** Public CLI calls carry caller attribution; installed clients own transport selection.
|
|
79
|
+
/** Public CLI calls carry caller attribution; installed clients own transport selection. A command acting as a board
|
|
80
|
+
* also names this process, the board's owner (the pid the machine daemon registered it by), so the daemon can hold
|
|
81
|
+
* the board's name to its owner (docs/architecture/overview.md, "Who is calling"). */
|
|
80
82
|
export function commandEnv(as = null) {
|
|
81
|
-
return { ...process.env, ...(as ? { SUPERCODE_CALLER: as } : {}) };
|
|
83
|
+
return { ...process.env, ...(as ? { SUPERCODE_CALLER: as, SUPERCODE_BOARD_OWNER_PID: String(process.pid) } : {}) };
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
package/board/git-document.mjs
CHANGED
|
@@ -91,21 +91,23 @@ const SETTLE_MS = 10_000;
|
|
|
91
91
|
* write awaits this first. A commit waiting out a busy index is not waited for: it stays owed, said. */
|
|
92
92
|
export async function settleDocuments(within = SETTLE_MS) {
|
|
93
93
|
const deadline = Date.now() + within;
|
|
94
|
+
// until none is left: a step that ran may have queued another (a commit its push follows); a retry whose timer fires
|
|
95
|
+
// meanwhile joins the chain and is waited for too
|
|
96
|
+
while (fetching.size || chains.size) {
|
|
97
|
+
const left = deadline - Date.now();
|
|
98
|
+
if (left <= 0) { current().err(`board: ${chains.size + fetching.size} git step(s) of the board documents were still running as this process ended; what they owed stays in the checkout for the next process\n`); break; }
|
|
99
|
+
await Promise.race([Promise.all([...fetching.values(), ...chains.values()]), new Promise((done) => setTimeout(done, left).unref?.())]);
|
|
100
|
+
}
|
|
101
|
+
// then what still waits on a timer, said: after the chains, so a retry a step set while settling is said too
|
|
94
102
|
for (const boards of boardsAt.values()) for (const board of boards) if (board.retryTimer) {
|
|
95
103
|
clearTimeout(board.retryTimer); board.retryTimer = null;
|
|
96
104
|
current().err(`board: ${board.owed.length} write${board.owed.length === 1 ? '' : 's'} of the board documents not committed as this process ends (git would not move HEAD); ${board.owed.length === 1 ? 'it stays' : 'they stay'} owed in the checkout (documents that differ from HEAD) for the next process\n`);
|
|
97
105
|
}
|
|
98
|
-
for (const [root, moves] of filesOwed) if (moves.timer) { clearTimeout(moves.timer); moves.timer = null; current().err(`board: ${moves.files.size} file(s) in ${root} still differ from HEAD as this process ends (what was pushed, not yet written there)\n`); }
|
|
106
|
+
for (const [root, moves] of filesOwed) if (moves.timer) { clearTimeout(moves.timer); moves.timer = null; current().err(`board: ${moves.files.size} file(s) in ${root} still differ from HEAD as this process ends (what was pushed, not yet written there); the next process to open a board there writes them\n`); }
|
|
99
107
|
for (const [root, sync] of indexOwed) if (sync.timer) {
|
|
100
108
|
clearTimeout(sync.timer); sync.timer = null;
|
|
101
109
|
current().err(`board: git's index in ${root} is still locked as this process ends; its board documents' index entries stay behind HEAD until the next process opens the board\n`);
|
|
102
110
|
}
|
|
103
|
-
// until none is left: a step that ran may have queued another (a commit its push follows)
|
|
104
|
-
while (fetching.size || chains.size) {
|
|
105
|
-
const left = deadline - Date.now();
|
|
106
|
-
if (left <= 0) { current().err(`board: ${chains.size + fetching.size} git step(s) of the board documents were still running as this process ended; what they owed stays in the checkout for the next process\n`); return; }
|
|
107
|
-
await Promise.race([Promise.all([...fetching.values(), ...chains.values()]), new Promise((done) => setTimeout(done, left).unref?.())]);
|
|
108
|
-
}
|
|
109
111
|
}
|
|
110
112
|
/** How long a busy index may hold an index sync before it is said (with its age), once per stretch. */
|
|
111
113
|
const BUSY_SAID_MS = 60_000;
|
|
@@ -153,9 +155,28 @@ const filesOwed = new Map();
|
|
|
153
155
|
function moveFiles(root, files, from, to) {
|
|
154
156
|
const moves = filesOwed.get(root) ?? { files: new Map(), since: null, said: false, delay: 0, timer: null };
|
|
155
157
|
filesOwed.set(root, moves);
|
|
156
|
-
|
|
158
|
+
// a file a second take moves again keeps the base of the first: its working text is still that one's until written
|
|
159
|
+
for (const file of files) moves.files.set(file, { from: moves.files.get(file)?.from ?? from, to });
|
|
160
|
+
keepMoves(root);
|
|
157
161
|
if (!moves.timer) onChain(root, () => moveNow(root));
|
|
158
162
|
}
|
|
163
|
+
/** The moves still owed, kept in the checkout's git dir until each is written: a process that ends first leaves them for
|
|
164
|
+
* the next one to open a board there (`derive`). */
|
|
165
|
+
const movesPath = (root) => { const checkout = checkoutOf(root); return checkout ? join(checkout.gitDir, 'supercode-board-moves.json') : null; };
|
|
166
|
+
function keepMoves(root) {
|
|
167
|
+
const path = movesPath(root); if (!path) return;
|
|
168
|
+
const moves = filesOwed.get(root);
|
|
169
|
+
try {
|
|
170
|
+
if (!moves?.files.size) rmSync(path, { force: true });
|
|
171
|
+
else writeAtomically(path, JSON.stringify(Object.fromEntries(moves.files)));
|
|
172
|
+
} catch (error) { current().err(`board: the moves owed in ${root} could not be kept (${error.message}); a process that ends before they are written leaves those files differing from HEAD\n`); }
|
|
173
|
+
}
|
|
174
|
+
/** Moves an earlier process left owed, queued again (once per checkout per process). */
|
|
175
|
+
function deriveMoves(root) {
|
|
176
|
+
const path = movesPath(root); if (!path) return;
|
|
177
|
+
let kept = null; try { kept = JSON.parse(readFileSync(path, 'utf8')); } catch { return; }
|
|
178
|
+
for (const [file, { from, to }] of Object.entries(kept ?? {})) if (from && to) moveFiles(root, [file], from, to);
|
|
179
|
+
}
|
|
159
180
|
async function moveNow(root) {
|
|
160
181
|
const moves = filesOwed.get(root);
|
|
161
182
|
if (!moves) return;
|
|
@@ -165,13 +186,26 @@ async function moveNow(root) {
|
|
|
165
186
|
const was = (await quiet(root, ['rev-parse', '-q', '--verify', `${from}:${file}`]))?.trim() || null;
|
|
166
187
|
let exists = true; try { statSync(join(root, file)); } catch { exists = false; }
|
|
167
188
|
const now = exists ? (await git(root, ['hash-object', '--', file])).trim() : null;
|
|
168
|
-
if (now !== was) {
|
|
189
|
+
if (now !== was) {
|
|
190
|
+
// already what the take brought (written before a process ended): done; else changed here since, left and said
|
|
191
|
+
const brought = (await quiet(root, ['rev-parse', '-q', '--verify', `${to}:${file}`]))?.trim() || null;
|
|
192
|
+
if (now !== brought) current().err(`board: ${file} in ${root} changed here since what was pushed was merged; it is left as it is (HEAD has the pushed text)\n`);
|
|
193
|
+
// written before a process ended whose index sync was still owed: the index entry follows HEAD now (a file HEAD
|
|
194
|
+
// lacks leaves the index, so it is not left staged against HEAD)
|
|
195
|
+
else await git(root, ['reset', '-q', 'HEAD', '--', file]);
|
|
196
|
+
moves.files.delete(file); continue;
|
|
197
|
+
}
|
|
169
198
|
const kept = Boolean((await quiet(root, ['rev-parse', '-q', '--verify', `${to}:${file}`]))?.trim());
|
|
170
199
|
if (kept) await git(root, ['checkout', to, '--', file]);
|
|
171
|
-
else
|
|
200
|
+
else {
|
|
201
|
+
// the index entry first (a busy index retries with the file still in place), then the file
|
|
202
|
+
await git(root, ['rm', '--cached', '-q', '--ignore-unmatch', '--', file]);
|
|
203
|
+
rmSync(join(root, file), { force: true });
|
|
204
|
+
}
|
|
172
205
|
moves.files.delete(file);
|
|
173
206
|
} catch (error) { failed ??= error; }
|
|
174
207
|
}
|
|
208
|
+
keepMoves(root);
|
|
175
209
|
if (!moves.files.size) { filesOwed.delete(root); return; }
|
|
176
210
|
moves.since ??= Date.now();
|
|
177
211
|
if (!moves.said && Date.now() - moves.since >= BUSY_SAID_MS) {
|
|
@@ -234,6 +268,7 @@ export class GitDocument {
|
|
|
234
268
|
syncIndex(this.root, this.paths.map((path) => this.rel(path)));
|
|
235
269
|
if (pushDerived.has(this.root)) return;
|
|
236
270
|
pushDerived.add(this.root);
|
|
271
|
+
deriveMoves(this.root);
|
|
237
272
|
const branch = this.branch();
|
|
238
273
|
if (branch) onChain(this.root, async () => { if (await this.unpushed(branch) && !this.pushing) this.pushInBackground(branch); });
|
|
239
274
|
}
|
package/board/runtime-door.mjs
CHANGED
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
import { machineCall } from './doors.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* One door call. `machine` null for this machine. `as`: the board this call acts for (`sc:<machine>:board:<name>`)
|
|
8
|
-
*
|
|
7
|
+
* One door call. `machine` null for this machine. `as`: the board this call acts for (`sc:<machine>:board:<name>`). To
|
|
8
|
+
* this machine's daemon it is this process's claim, naming the board, from the board's owner (this process, the pid the
|
|
9
|
+
* daemon registered for its home); to another machine, the board's name as the caller, which that machine takes from
|
|
10
|
+
* this machine's link (machine.mjs #callerOf).
|
|
9
11
|
*/
|
|
10
12
|
export async function runtimeDoor(machine, method, params = {}, { timeout = 60_000, as = null } = {}) {
|
|
11
13
|
try {
|
|
12
14
|
if (!method.startsWith('harness.v1.')) throw Object.assign(new Error('the runtime door accepts harness.v1 methods'), { code: 'invalid_request' });
|
|
13
|
-
const
|
|
15
|
+
const acting = as && !method.startsWith('harness.v1.sessions.');
|
|
16
|
+
const named = !acting ? params
|
|
17
|
+
: machine ? { ...params, caller: as }
|
|
18
|
+
: { ...params, claim: { pid: process.pid, ppid: process.ppid, session: null, named: as, owner: String(process.pid) } };
|
|
14
19
|
const result = await machineCall(machine, method, named, { timeoutMs: timeout });
|
|
15
20
|
return { code: 0, stdout: `${JSON.stringify(result ?? null)}\n`, stderr: '' };
|
|
16
21
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.87",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.60",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.32",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|