@volter/supercode-orchestrator 0.5.73 → 0.5.75
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/agents.mjs +34 -24
- package/bin/orchestrator.mjs +2 -0
- package/board/agent-declarations.mjs +5 -5
- package/board/ci.mjs +98 -102
- package/board/cli.mjs +44 -57
- package/board/dispatch.mjs +314 -190
- package/board/engine.mjs +50 -28
- package/board/git-document.mjs +68 -20
- package/board/machine-fleet.mjs +27 -14
- package/board/runtime-door.mjs +17 -14
- package/board/store.mjs +1 -1
- package/board/tools.mjs +2 -2
- package/board/workflow.mjs +13 -12
- package/board/workspace.mjs +22 -15
- package/board/ztrack.mjs +22 -12
- package/hermes-cli.mjs +4 -12
- package/loop.mjs +7 -7
- package/package.json +2 -2
- package/bin/machine-fleet.mjs +0 -38
- package/bin/runtime-door.mjs +0 -23
package/board/workspace.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const isWorkspacePath = value => /^(\/|~|[A-Za-z]:[\\/])/.test(value ?? '
|
|
|
18
18
|
// The dispatcher's latest presence reading (observeFleet): a machine it reads as offline is not asked.
|
|
19
19
|
let presence = null;
|
|
20
20
|
export const notePresence = (reading) => { presence = reading; };
|
|
21
|
-
function workspaceDoor(machine, verb, value, options = {}) {
|
|
21
|
+
async function workspaceDoor(machine, verb, value, options = {}) {
|
|
22
22
|
if (machine && presence?.known === true && normalMachine(machine) !== localMachine() && !presence.online.has(normalMachine(machine))) {
|
|
23
23
|
throw new Error(`workspace on ${machine}: machine ${normalMachine(machine)} is offline`);
|
|
24
24
|
}
|
|
@@ -27,39 +27,46 @@ function workspaceDoor(machine, verb, value, options = {}) {
|
|
|
27
27
|
// none on a volume macOS guards, where the session that named the path does, and a verdict waited on the daemon's
|
|
28
28
|
// EPERM. The daemon is asked when this process cannot read it.
|
|
29
29
|
let own = null;
|
|
30
|
-
if (local && verb === 'inspect') { try { return inspectCheckout(value); } catch (error) { own = error; } }
|
|
30
|
+
if (local && verb === 'inspect') { try { return await inspectCheckout(value); } catch (error) { own = error; } }
|
|
31
31
|
const params = verb === 'list' ? {} : verb === 'inspect' || verb === 'register' ? {path:value} : {repository:value,...options};
|
|
32
|
-
const r = runtimeDoor(local?null:machine,`harness.v1.workspaces.${verb}`,params,{env:commandEnv()});
|
|
32
|
+
const r = await runtimeDoor(local?null:machine,`harness.v1.workspaces.${verb}`,params,{env:commandEnv()});
|
|
33
33
|
if (r.code !== 0) throw new Error(`workspace on ${machine}: ${own ? `${own.message}; the machine daemon: ` : ''}${(r.stderr || r.stdout || r.error?.message).trim()}`);
|
|
34
34
|
return JSON.parse(r.stdout);
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** A workspace declaration that asks no machine (scratch, or a repository named by id), else null: a path is declared
|
|
37
|
+
* through its machine's door (declareWorkspace). */
|
|
38
|
+
export function plainWorkspace(kind, value, branch = null) {
|
|
37
39
|
if (kind === 'scratch') return { repository: null, branch: null, path: null };
|
|
38
40
|
if (!value) throw new Error('workspace names a repository, for example worktree:volter-ai/editor');
|
|
39
41
|
if (!isWorkspacePath(value)) return { repository: repositoryId(value), branch, path: null };
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
export async function declareWorkspace(kind, value, machine, branch = null) {
|
|
45
|
+
const plain = plainWorkspace(kind, value, branch);
|
|
46
|
+
if (plain) return plain;
|
|
40
47
|
// A legacy path supplied explicitly is checked and registered on its machine, never copied into the card.
|
|
41
|
-
const info = workspaceDoor(machine, 'inspect', value);
|
|
48
|
+
const info = await workspaceDoor(machine, 'inspect', value);
|
|
42
49
|
if (kind === 'worktree' && info.linked) throw new Error('worktree workspace already names a linked worktree; name its repository for a solo worktree');
|
|
43
50
|
if (kind === 'dir' && !info.branch && !branch) throw new Error('a detached checkout has no portable branch identity; name the repository and a solo worktree branch');
|
|
44
|
-
const row = workspaceDoor(machine, 'register', value);
|
|
51
|
+
const row = await workspaceDoor(machine, 'register', value);
|
|
45
52
|
return { repository: row.repository, branch: branch ?? (kind === 'dir' ? row.branch : null), path: kind === 'dir' ? row.path : null };
|
|
46
53
|
}
|
|
47
54
|
/** The checkouts this machine keeps of any repository on `branch`: where an arc changing several repositories
|
|
48
55
|
* keeps its work in each of them (its own worktree aside). Another machine's map is not read from here. */
|
|
49
|
-
export function branchCheckouts(machine, branch) {
|
|
56
|
+
export async function branchCheckouts(machine, branch) {
|
|
50
57
|
if (!branch || (machine && normalMachine(machine) !== localMachine())) return [];
|
|
51
|
-
const listed = workspaceDoor(null, 'list', null);
|
|
58
|
+
const listed = await workspaceDoor(null, 'list', null);
|
|
52
59
|
return (Array.isArray(listed) ? listed : listed.workspaces ?? listed.items ?? []).filter((row) => (branch === '*' || row.branch === branch) && existsSync(row.path));
|
|
53
60
|
}
|
|
54
61
|
/** The registered checkout of `repository` on `branch` on `machine`: where a card changing several repositories
|
|
55
62
|
* keeps its work in each of them. */
|
|
56
|
-
export function checkoutOf(machine, repository, branch) {
|
|
63
|
+
export async function checkoutOf(machine, repository, branch) {
|
|
57
64
|
return workspaceDoor(machine, 'resolve', repositoryId(repository), { branch });
|
|
58
65
|
}
|
|
59
|
-
export function resolveWorkspace(task, create = false) {
|
|
66
|
+
export async function resolveWorkspace(task, create = false) {
|
|
60
67
|
const declared = task.workspace_anchor ?? task.workspace_path;
|
|
61
68
|
if (isWorkspacePath(declared)) {
|
|
62
|
-
const info = workspaceDoor(task.machine, 'inspect', declared);
|
|
69
|
+
const info = await workspaceDoor(task.machine, 'inspect', declared);
|
|
63
70
|
if (task.workspace_kind === 'dir') return info;
|
|
64
71
|
if (info.linked) throw new Error('worktree declaration is already a linked worktree; name its repository instead');
|
|
65
72
|
throw new Error('legacy worktree path needs registration and a repository declaration before launch');
|
|
@@ -70,7 +77,7 @@ export function resolveWorkspace(task, create = false) {
|
|
|
70
77
|
if (task.workspace_kind === 'worktree' && task.workspace_path &&
|
|
71
78
|
basename(task.workspace_path) === task.id && basename(dirname(task.workspace_path)) === '.worktrees' &&
|
|
72
79
|
(!isLocalWorkspace(task) || existsSync(task.workspace_path))) {
|
|
73
|
-
const cached = workspaceDoor(task.machine, 'inspect', task.workspace_path);
|
|
80
|
+
const cached = await workspaceDoor(task.machine, 'inspect', task.workspace_path);
|
|
74
81
|
if (cached.linked && basename(cached.path) === task.id && basename(dirname(cached.path)) === '.worktrees' &&
|
|
75
82
|
cached.repository === repositoryId(declared) && cached.branch === (task.branch_name ?? `wt/${task.id}`)) {
|
|
76
83
|
return { ...cached, root: dirname(dirname(cached.path)) };
|
|
@@ -79,14 +86,14 @@ export function resolveWorkspace(task, create = false) {
|
|
|
79
86
|
}
|
|
80
87
|
return workspaceDoor(task.machine, 'resolve', declared, { branch: task.branch_name, solo: task.workspace_kind === 'worktree', card: task.id, create });
|
|
81
88
|
}
|
|
82
|
-
export function checkWorkspace(board, task) {
|
|
89
|
+
export async function checkWorkspace(board, task) {
|
|
83
90
|
if (task.workspace_kind === 'scratch') return null;
|
|
84
|
-
const facts = resolveWorkspace(task), peers = [];
|
|
91
|
+
const facts = await resolveWorkspace(task), peers = [];
|
|
85
92
|
if (task.workspace_kind === 'dir' && !facts.linked) {
|
|
86
93
|
for (const row of board.db.prepare(`SELECT t.id,t.workspace_path,t.branch_name,c.workspace_anchor,c.machine FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.current_run_id IS NOT NULL AND t.workspace_kind='dir'`).all()) {
|
|
87
94
|
if (row.id === task.id || normalMachine(row.machine ?? localMachine()) !== normalMachine(task.machine ?? localMachine())) continue;
|
|
88
95
|
let other;
|
|
89
|
-
try { other = resolveWorkspace({ ...row, workspace_kind: 'dir' }); } catch { continue; } // That card's own launch reports its invalid workspace.
|
|
96
|
+
try { other = await resolveWorkspace({ ...row, workspace_kind: 'dir' }); } catch { continue; } // That card's own launch reports its invalid workspace.
|
|
90
97
|
if (!other.linked && other.root === facts.root) peers.push(row.id);
|
|
91
98
|
}
|
|
92
99
|
}
|
package/board/ztrack.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
17
17
|
import { actorOf, contention, event, json, now, principalOf } from './store.mjs';
|
|
18
18
|
import { GitDocument } from './git-document.mjs';
|
|
19
19
|
import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
|
|
20
|
+
import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
|
|
20
21
|
|
|
21
22
|
/** Run `fn` as a named step of the running command (the slow log names its slowest step). */
|
|
22
23
|
const timedStep = (step, fn) => { const started = performance.now(); try { return fn(); } finally { noteStep(step, performance.now() - started); } };
|
|
@@ -100,7 +101,7 @@ class ZtrackBoard {
|
|
|
100
101
|
const said = current().said;
|
|
101
102
|
if (this.git && !said.has(`behind:${this.path}`)) {
|
|
102
103
|
said.add(`behind:${this.path}`);
|
|
103
|
-
const late = this.git.behind();
|
|
104
|
+
const late = timedStep('board open git behind', () => this.git.behind());
|
|
104
105
|
// A push younger than a minute is still on its way.
|
|
105
106
|
if (late && Date.now() - late.since > 60_000) current().err(`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`);
|
|
106
107
|
}
|
|
@@ -112,12 +113,12 @@ class ZtrackBoard {
|
|
|
112
113
|
// `sqlite3 <store> "select …"` made the empty file the next release then opened instead of importing).
|
|
113
114
|
if (existsSync(this.file) && statSync(this.file).size === 0) unlinkSync(this.file);
|
|
114
115
|
if (!existsSync(this.file)) this.create(legacy);
|
|
115
|
-
this.conn = new DatabaseSync(this.file);
|
|
116
|
+
this.conn = timedStep('board open connection', () => new DatabaseSync(this.file));
|
|
116
117
|
this.refuseUnimported(legacy);
|
|
117
118
|
this.errors = new Map(); this.statements = new Map(); this.hashes = new Map();
|
|
118
119
|
this.active = false; this.overlay = false; this.seen = null; this.refused = new Set();
|
|
119
|
-
this.prepareConnection();
|
|
120
|
-
this.loadErrors();
|
|
120
|
+
timedStep('board open schema', () => this.prepareConnection());
|
|
121
|
+
timedStep('board open errors', () => this.loadErrors());
|
|
121
122
|
const self = this;
|
|
122
123
|
this.db = {
|
|
123
124
|
boardErrors: self.errors,
|
|
@@ -132,7 +133,9 @@ class ZtrackBoard {
|
|
|
132
133
|
exec(sql) { return self.active ? self.noting(() => self.conn.exec(sql)) : self.tx(() => self.conn.exec(sql)); },
|
|
133
134
|
close() { self.close(); },
|
|
134
135
|
};
|
|
135
|
-
|
|
136
|
+
// A board's first open in this process recovers what its store accepted and derives what git owes for it (tx), so
|
|
137
|
+
// a board that is only read, or whose owner runs no round, still finishes an earlier process's commit.
|
|
138
|
+
if (seed || this.git?.underived) this.tx(() => {});
|
|
136
139
|
}
|
|
137
140
|
// The board's documents: the open one, the current archive, and each rotated archive (recorded in the board's own
|
|
138
141
|
// state, so a rotation committed but not yet written is still a document of the board).
|
|
@@ -394,13 +397,12 @@ class ZtrackBoard {
|
|
|
394
397
|
try {
|
|
395
398
|
txStarted = performance.now();
|
|
396
399
|
this.dropOverlay();
|
|
397
|
-
this.conn.exec('BEGIN');
|
|
398
|
-
fenced(this.conn);
|
|
400
|
+
timedStep('board tx begin', () => { this.conn.exec('BEGIN'); fenced(this.conn); });
|
|
399
401
|
this.active = true; this.contended = false;
|
|
400
402
|
this.conn.exec('DELETE FROM temp.board_dirty; DELETE FROM temp.board_touched');
|
|
401
403
|
this.refused = new Set();
|
|
402
404
|
timedStep('board tx errors', () => this.loadErrors());
|
|
403
|
-
timedStep('board tx recover', () => this.recover());
|
|
405
|
+
const recovered = timedStep('board tx recover', () => this.recover());
|
|
404
406
|
if (this.seed) this.indexForSeed();
|
|
405
407
|
else {
|
|
406
408
|
const stale = timedStep('board tx stale check', () => this.staleDocuments({ pending: false }));
|
|
@@ -416,13 +418,13 @@ class ZtrackBoard {
|
|
|
416
418
|
this.put('pending', { intent, files });
|
|
417
419
|
this.put('pending_hashes', Object.fromEntries(files.map((f) => [f.role, { before: f.before, after: f.after }])));
|
|
418
420
|
}
|
|
419
|
-
this.conn.exec('COMMIT');
|
|
421
|
+
timedStep('board tx commit', () => this.conn.exec('COMMIT'));
|
|
420
422
|
// A long transaction holds every other writer of this board (the dispatcher's tick included), so it is a line
|
|
421
423
|
// of its own in the slow log, from whichever process ran it.
|
|
422
424
|
const held = performance.now() - txStarted;
|
|
423
425
|
noteStep('board transaction', held);
|
|
424
|
-
recordSlow({ kind: 'call', name: 'board transaction', ms: held, slowest: null, extra: { board: this.path } });
|
|
425
|
-
outcome = { result, files, intent };
|
|
426
|
+
recordSlow({ kind: 'call', name: 'board transaction', ms: held, slowest: null, extra: { board: this.path, steps: slowLog.stepsWithin?.(txStarted, performance.now()) ?? null } });
|
|
427
|
+
outcome = { result, files, intent, recovered };
|
|
426
428
|
} catch (error) {
|
|
427
429
|
try { this.conn.exec('ROLLBACK'); } catch { /* not begun */ }
|
|
428
430
|
this.active = false; this.seen = null; this.loadErrors();
|
|
@@ -430,6 +432,13 @@ class ZtrackBoard {
|
|
|
430
432
|
throw error;
|
|
431
433
|
} finally { this.active = false; }
|
|
432
434
|
this.seed = null;
|
|
435
|
+
// What an earlier process left owed in git is derived once this board's recovery has written what its store
|
|
436
|
+
// accepted, with every document the board has; a recovery in any later write is committed the same way.
|
|
437
|
+
if (this.git && (this.git.underived || outcome.recovered)) {
|
|
438
|
+
this.git.paths = this.docs.map((doc) => doc.path);
|
|
439
|
+
try { timedStep('board git derive', () => outcome.recovered && !this.git.underived ? this.git.commitDirty('board: documents another write accepted') : this.git.derive()); }
|
|
440
|
+
catch (error) { current().err(`board: ${error.message}\n`); }
|
|
441
|
+
}
|
|
433
442
|
if (outcome.intent) {
|
|
434
443
|
this.publish(outcome);
|
|
435
444
|
// Committed as the session (or person) whose act this write was, and pushed.
|
|
@@ -471,7 +480,7 @@ class ZtrackBoard {
|
|
|
471
480
|
// document it replaced; merge it into a document edited since (the hand edit wins its cards).
|
|
472
481
|
recover() {
|
|
473
482
|
const pending = this.get('pending');
|
|
474
|
-
if (!pending) return;
|
|
483
|
+
if (!pending) return false;
|
|
475
484
|
for (const file of pending.files) {
|
|
476
485
|
const current = this.fileHash(file.path);
|
|
477
486
|
if (current.hash === file.after) continue;
|
|
@@ -483,6 +492,7 @@ class ZtrackBoard {
|
|
|
483
492
|
if (merged) atomic(file.path, merged.text);
|
|
484
493
|
}
|
|
485
494
|
this.drop('pending'); this.drop('pending_hashes');
|
|
495
|
+
return true;
|
|
486
496
|
}
|
|
487
497
|
|
|
488
498
|
// ------------------------------------------------------------------ the document index
|
package/hermes-cli.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// pinned `hermes` on its `PATH`; and what Hermes's scheduler records about a failed run (its incidents,
|
|
5
5
|
// in `cron/executions.db`) the orchestrator records through Hermes's own function.
|
|
6
6
|
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
-
import { dirname, join } from 'node:path';
|
|
7
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
9
9
|
import { parseDotenv } from './orchestration.mjs';
|
|
10
10
|
import { locateHermes } from './apply/doors.mjs';
|
|
11
11
|
|
|
@@ -28,7 +28,8 @@ let located;
|
|
|
28
28
|
export function hermesInstall() {
|
|
29
29
|
if (located !== undefined) return located;
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
// `hermes` on PATH, found by the file system (no shell started for it)
|
|
32
|
+
const which = String(process.env.PATH ?? '').split(delimiter).filter(Boolean).map((dir) => join(dir, 'hermes')).find((path) => { try { accessSync(path, constants.X_OK); return statSync(path).isFile(); } catch { return false; } }) ?? '';
|
|
32
33
|
located = which ? { bin: dirname(which), ...locateHermes() } : null;
|
|
33
34
|
} catch {
|
|
34
35
|
located = null;
|
|
@@ -350,15 +351,6 @@ export async function memorySnapshot(profileDir) {
|
|
|
350
351
|
return runHermes(profileDir, { op: 'snapshot' });
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
/** memorySnapshot for a synchronous caller (the board's launch). An unreadable store contributes nothing. */
|
|
354
|
-
export function memorySnapshotSync(profileDir) {
|
|
355
|
-
const install = hermesInstall();
|
|
356
|
-
if (!install?.python) return '';
|
|
357
|
-
const run = spawnSync(install.python, ['-c', HERMES_TOOL], { input: JSON.stringify({ op: 'snapshot' }), encoding: 'utf8', timeout: 30_000, cwd: install.source, env: hermesEnv(install, profileDir) });
|
|
358
|
-
try { const out = hermesAnswer('snapshot', { stdout: run.stdout ?? '', stderr: run.stderr || run.error?.message || '' }); return typeof out === 'string' ? out : ''; }
|
|
359
|
-
catch { return ''; }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
354
|
/**
|
|
363
355
|
* A schedule string as Hermes parses it (`{ kind: once|interval|cron, … }`), or null when no Hermes is
|
|
364
356
|
* installed; throws Hermes's own refusal. Synchronous: schedules are parsed where a job is built.
|
package/loop.mjs
CHANGED
|
@@ -180,16 +180,16 @@ export class Orchestrator {
|
|
|
180
180
|
tick(at = this.clock()) {
|
|
181
181
|
this.queue = this.queue.then(() => this.adoptOutsideJobs()).catch((error) => this.log('warn', `cron/jobs.json not read back: ${error.message}`));
|
|
182
182
|
return this.dispatch(events.tick(at, cronInactivitySeconds(), cronMaxParallel())).then(
|
|
183
|
-
(result) => { this.tickerHeartbeat(true); this.declareLocalAgents();return result; },
|
|
183
|
+
(result) => { this.tickerHeartbeat(true); void this.declareLocalAgents().catch((error) => this.log('warn', `agent hand-off: ${error.message}`));return result; },
|
|
184
184
|
(error) => { this.tickerHeartbeat(false); throw error; },
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/** The home's agent declarations and ends a launch could not hand to this machine's daemon (none was running) are
|
|
189
189
|
* handed on. The daemon declares and ends them in Teams and binds their sessions; the gateway makes no Teams call. */
|
|
190
|
-
declareLocalAgents(){
|
|
190
|
+
async declareLocalAgents(){
|
|
191
191
|
if(!existsSync(join(this.state.root,'agent-state.sqlite')))return;
|
|
192
|
-
try{for(const pending of handOffAgents(this.state.root))this.log('warn',`agent ${pending.agent}: ${pending.error}`);}
|
|
192
|
+
try{for(const pending of await handOffAgents(this.state.root))this.log('warn',`agent ${pending.agent}: ${pending.error}`);}
|
|
193
193
|
catch(error){this.log('warn',`agent hand-off: ${error.message}`);}
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -416,8 +416,8 @@ export class Orchestrator {
|
|
|
416
416
|
if(!record)throw new Error('launch as an agent requires its retained profile intent');
|
|
417
417
|
agent=record;
|
|
418
418
|
}
|
|
419
|
-
agent??=localProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,agent:agentName,declarations:this.state.agents});
|
|
420
|
-
if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
419
|
+
agent??=await localProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,agent:agentName,declarations:this.state.agents});
|
|
420
|
+
if(!scope.offline)for(const pending of await handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
421
421
|
agent.intended.resolved.worker={...agent.intended.resolved.worker,...worker,cwd};
|
|
422
422
|
main=new_fork||!agent.identity.main_session;
|
|
423
423
|
if(resume_runtime_id&&!new_fork){
|
|
@@ -507,8 +507,8 @@ export class Orchestrator {
|
|
|
507
507
|
if(this.runtimes.has(leaseId(profileName,keyString)))throw new Error('fork requires an unoccupied target conversation');
|
|
508
508
|
if(!(await this.descriptorFor(locator.harness))?.runtime?.capabilities?.resume_session)throw new Error('this harness cannot resume a materialized fork');
|
|
509
509
|
const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context),session=`${locator.harness}:${locator.session_id}`;
|
|
510
|
-
const child=localForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key});
|
|
511
|
-
if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
510
|
+
const child=await localForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key});
|
|
511
|
+
if(!scope.offline)for(const pending of await handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
512
512
|
const runKey=`fork:${key}`,prior=agentRunForKey(this.state.root,scope.org,child.identity.id,runKey);
|
|
513
513
|
let target;
|
|
514
514
|
if(prior) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.75",
|
|
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.25",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|
package/bin/machine-fleet.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// One fleet reading for a synchronous board process: each named machine's panes and the harness session under each live
|
|
3
|
-
// pane, every machine read at once through its own door, each bounded by its own deadline. Prints one row per machine:
|
|
4
|
-
// { machine, panes, activities } when its door answered, { machine, error, code } when it did not.
|
|
5
|
-
import { openMachine } from '@volter/teams/machine-door';
|
|
6
|
-
import { StreamRpc, harnessTransport } from '@volter/teams/client';
|
|
7
|
-
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
8
|
-
let input = ''; for await (const chunk of process.stdin) input += chunk;
|
|
9
|
-
const { machines, deadlineMs } = JSON.parse(input);
|
|
10
|
-
const within = (promise, ms) => Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(Object.assign(new Error(`no answer within ${ms} ms`), { code: 'timeout' })), ms))]);
|
|
11
|
-
async function read({ name, at }) {
|
|
12
|
-
let channel;
|
|
13
|
-
try {
|
|
14
|
-
channel = await openMachine({ machine: at });
|
|
15
|
-
const node = new StreamRpc(await channel.open('node'));
|
|
16
|
-
const panes = await node.call('harness.v1.panes.list', {});
|
|
17
|
-
// What a board launch needs of this machine: its board process guard (describe), and its keyed launch door, asked
|
|
18
|
-
// for a key no launch has (an answer of null is the door; an unknown method is its absence)
|
|
19
|
-
const description = await node.call('harness.v1.machine.describe', {}).catch(() => null);
|
|
20
|
-
const keyed = await node.call('harness.v1.launches.get', { key: 'board-capability-reading' }).then(() => null, (error) => String(error?.message ?? error).slice(0, 200));
|
|
21
|
-
const capabilities = { guard: description?.boardProcessPolicy?.version === 1 ? description.boardProcessPolicy.harnesses ?? [] : [], launches: keyed === null, launchesRefused: keyed };
|
|
22
|
-
const live = (Array.isArray(panes) ? panes : []).filter((pane) => !pane.dead && pane.pid);
|
|
23
|
-
// the session under each live pane: the harness stream, an operator's; a caller the machine admits only to its panes
|
|
24
|
-
// (a box another custodian enrolled, granting view and control) reads its panes and no sessions
|
|
25
|
-
let activities = [], sessionsUnread = null;
|
|
26
|
-
if (live.length) {
|
|
27
|
-
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
|
28
|
-
try { await client.start?.(); activities = (await client.request('harness.v1.sessions.activity_under', { pids: live.map((pane) => pane.pid) }))?.activities ?? []; }
|
|
29
|
-
catch (error) { sessionsUnread = String(error?.message ?? error).slice(0, 200); }
|
|
30
|
-
finally { await client.close?.().catch(() => {}); }
|
|
31
|
-
}
|
|
32
|
-
return { machine: name, panes, activities, capabilities, sessionsUnread };
|
|
33
|
-
} finally { channel?.close(); }
|
|
34
|
-
}
|
|
35
|
-
const rows = await Promise.all(machines.map((machine) => within(read(machine), deadlineMs)
|
|
36
|
-
.catch((error) => ({ machine: machine.name, error: String(error?.message ?? error).slice(0, 300), code: error?.code ?? 'unavailable' }))));
|
|
37
|
-
process.stdout.write(`${JSON.stringify(rows)}\n`);
|
|
38
|
-
process.exit(0);
|
package/bin/runtime-door.mjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// A synchronous board process delegates one request to the public daemon door.
|
|
3
|
-
// This helper owns neither credentials nor machine state; the SDK opens the admitted channel.
|
|
4
|
-
import { openMachine } from '@volter/teams/machine-door';
|
|
5
|
-
import { StreamRpc, harnessTransport } from '@volter/teams/client';
|
|
6
|
-
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
7
|
-
let input = ''; for await (const chunk of process.stdin) input += chunk;
|
|
8
|
-
let channel;
|
|
9
|
-
try {
|
|
10
|
-
const { machine, method, params = {} } = JSON.parse(input);
|
|
11
|
-
if (!method.startsWith('harness.v1.')) throw new Error('the runtime door accepts harness.v1 methods');
|
|
12
|
-
channel = await openMachine({machine});
|
|
13
|
-
let result;
|
|
14
|
-
if (method.startsWith('harness.v1.sessions.')) {
|
|
15
|
-
// A session's records are the harness's own service, spoken by its SDK over the machine's harness stream.
|
|
16
|
-
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
|
17
|
-
try { await client.start?.(); result = await client.request(method, params); } finally { await client.close?.(); }
|
|
18
|
-
} else result = await new StreamRpc(await channel.open('node')).call(method,params);
|
|
19
|
-
process.stdout.write(JSON.stringify(result)+'\n');
|
|
20
|
-
} catch (error) {
|
|
21
|
-
process.stderr.write(JSON.stringify({code:error.code??'unavailable',message:error.message})+'\n');
|
|
22
|
-
process.exitCode = 1;
|
|
23
|
-
} finally { channel?.close(); }
|