@volter/supercode-orchestrator 0.5.37 → 0.5.39
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 +36 -7
- package/bin/machine-fleet.mjs +29 -0
- package/board/cli.mjs +12 -2
- package/board/dispatch.mjs +260 -108
- package/board/engine.mjs +103 -32
- package/board/machine-fleet.mjs +49 -0
- package/board/request-mail.mjs +3 -1
- package/board/store.mjs +5 -0
- package/board/wake.mjs +2 -4
- package/board.mjs +3 -1
- package/loop.mjs +5 -4
- package/package.json +1 -1
- package/board/event-fleet.mjs +0 -66
package/agents.mjs
CHANGED
|
@@ -38,6 +38,11 @@ function store(root){
|
|
|
38
38
|
CREATE TABLE IF NOT EXISTS agent_end_outbox(org TEXT NOT NULL,agent_id TEXT NOT NULL,reason TEXT NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(org,agent_id));
|
|
39
39
|
CREATE TABLE IF NOT EXISTS agent_runtime_sessions(org TEXT NOT NULL,runtime_ref TEXT NOT NULL,session_ref TEXT NOT NULL,PRIMARY KEY(org,runtime_ref));
|
|
40
40
|
CREATE TABLE IF NOT EXISTS agent_runs(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,machine TEXT,run_id TEXT,profile_json TEXT,published INTEGER NOT NULL DEFAULT 0,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));`);
|
|
41
|
+
// A declaration Teams refused is asked again a bounded number of times (synchronizeLocalAgents), then left failed.
|
|
42
|
+
const columns=new Set(db.prepare('PRAGMA table_info(agent_intents)').all().map(c=>c.name));
|
|
43
|
+
for(const [column,type] of Object.entries({declare_attempts:'INTEGER NOT NULL DEFAULT 0',declare_next_at:'INTEGER NOT NULL DEFAULT 0',declare_failed_at:'INTEGER',declare_error:'TEXT'}))if(!columns.has(column))db.exec(`ALTER TABLE agent_intents ADD COLUMN ${column} ${type}`);
|
|
44
|
+
// An end is claimed by the pass sending it (synchronizeAgentEnds).
|
|
45
|
+
if(!new Set(db.prepare('PRAGMA table_info(agent_end_outbox)').all().map(c=>c.name)).has('claimed_at'))db.exec('ALTER TABLE agent_end_outbox ADD COLUMN claimed_at INTEGER');
|
|
41
46
|
return db;
|
|
42
47
|
}
|
|
43
48
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
@@ -86,13 +91,16 @@ export function synchronizeAgentEnds(root,org,{context}={}){
|
|
|
86
91
|
try{
|
|
87
92
|
for(const row of db.prepare('SELECT * FROM agent_end_outbox WHERE org=? ORDER BY created_at LIMIT 100').all(org)){
|
|
88
93
|
if(db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(org,row.agent_id)){db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,row.agent_id);continue;}
|
|
89
|
-
if(db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND published
|
|
94
|
+
if(db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND published<=0').get(org,row.agent_id)){pending.push({agent:row.agent_id,error:'session ownership publication remains pending'});continue;}
|
|
95
|
+
// claimed by compare-and-set on its claimed_at (free, or a claim older than SYNC_CLAIM_MS from a pass that died);
|
|
96
|
+
// deleted only after Teams ended the agent (the end is idempotent by its digest), handed back when it failed
|
|
97
|
+
if(db.prepare('UPDATE agent_end_outbox SET claimed_at=? WHERE org=? AND agent_id=? AND (claimed_at IS NULL OR claimed_at<?)').run(Date.now(),org,row.agent_id,Date.now()-SYNC_CLAIM_MS).changes!==1)continue;
|
|
90
98
|
try{
|
|
91
99
|
agentDoor('end',[row.agent_id,row.reason,digest(`${org}/${row.agent_id}/end/${row.created_at}`)],{context});
|
|
92
100
|
db.prepare('UPDATE agent_runs SET ended_at=COALESCE(ended_at,?),end_reason=COALESCE(end_reason,?) WHERE org=? AND agent_id=?').run(Date.now(),row.reason,org,row.agent_id);
|
|
93
101
|
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,row.agent_id);
|
|
94
102
|
}
|
|
95
|
-
catch(error){pending.push({agent:row.agent_id,error:error.message});}
|
|
103
|
+
catch(error){db.prepare('UPDATE agent_end_outbox SET claimed_at=NULL WHERE org=? AND agent_id=?').run(org,row.agent_id);pending.push({agent:row.agent_id,error:error.message});}
|
|
96
104
|
}
|
|
97
105
|
return pending;
|
|
98
106
|
}finally{db.close();}
|
|
@@ -349,21 +357,36 @@ function adoptLocalOrg(root,scope){
|
|
|
349
357
|
db.exec('COMMIT');
|
|
350
358
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
351
359
|
}
|
|
360
|
+
const DECLARE_ATTEMPTS=6;
|
|
361
|
+
// A sync claim outlasts one Teams call chain (each door call bounded at 25-60 s).
|
|
362
|
+
const SYNC_CLAIM_MS=600000;
|
|
352
363
|
/** Declare every locally minted identity once Teams is reachable, under the id its sessions already carry. */
|
|
353
364
|
export function synchronizeLocalAgents({root,profiles,context,machine=null,declarations={}}){
|
|
354
365
|
const scope=agentScope(context);if(scope.offline)return [];
|
|
355
366
|
adoptLocalOrg(root,scope);
|
|
356
367
|
const db=store(root);let rows;
|
|
357
|
-
try{rows=db.prepare('SELECT * FROM agent_intents WHERE org=? AND declared=0 AND declaration_json IS NOT NULL ORDER BY created_at').all(scope.org);}finally{db.close();}
|
|
368
|
+
try{rows=db.prepare('SELECT * FROM agent_intents WHERE org=? AND declared=0 AND declaration_json IS NOT NULL AND declare_failed_at IS NULL AND declare_next_at<=? ORDER BY created_at').all(scope.org,Date.now());}finally{db.close();}
|
|
358
369
|
const pending=[],base={root,org:scope.org,server:scope.server,profiles,context,machine};
|
|
359
370
|
for(const row of rows){
|
|
360
371
|
const declaration=JSON.parse(row.declaration_json);
|
|
372
|
+
// claimed by compare-and-set on its own next attempt time, set ahead only while it is due: a second pass skips it
|
|
373
|
+
const claim=store(root);let claimed;
|
|
374
|
+
try{claimed=claim.prepare('UPDATE agent_intents SET declare_next_at=? WHERE org=? AND id=? AND declared=0 AND declare_failed_at IS NULL AND declare_next_at<=?').run(Date.now()+SYNC_CLAIM_MS,row.org,row.id,Date.now()).changes===1;}finally{claim.close();}
|
|
375
|
+
if(!claimed)continue;
|
|
361
376
|
try{
|
|
362
377
|
if(declaration.kind==='root')ensureRootAgent({...base,profile:declaration.profile});
|
|
363
378
|
else if(declaration.kind==='card')ensureCardAgent({...base,profile:declaration.profile,card:declaration.card,role:declaration.role,sponsor:declaration.sponsor,starterSession:declaration.starterSession});
|
|
364
379
|
else if(declaration.kind==='profile')ensureProfileAgent({...base,profile:declaration.profile});
|
|
365
380
|
else if(declaration.kind==='declared')ensureProfileAgent({...base,profile:declaration.profile,agent:declaration.agent,declarations});
|
|
366
|
-
}catch(error){
|
|
381
|
+
}catch(error){
|
|
382
|
+
// Teams unreachable is no answer about this declaration: the rest wait for the next pass, and no attempt is counted.
|
|
383
|
+
if(teamsUnavailable(error)){const db=store(root);try{db.prepare('UPDATE agent_intents SET declare_next_at=? WHERE org=? AND id=?').run(row.declare_next_at??0,row.org,row.id);}finally{db.close();}return [...pending,{agent:row.id,error:error.message}];}
|
|
384
|
+
// A refusal is this declaration's own: asked again after a doubling wait (one minute, at most an hour), and after
|
|
385
|
+
// DECLARE_ATTEMPTS refusals left failed, said once, and never asked again.
|
|
386
|
+
const made=Number(row.declare_attempts??0)+1,failed=made>=DECLARE_ATTEMPTS,db=store(root);
|
|
387
|
+
try{db.prepare('UPDATE agent_intents SET declare_attempts=?,declare_next_at=?,declare_failed_at=?,declare_error=? WHERE org=? AND id=?').run(made,Date.now()+Math.min(3600000,60000*2**(made-1)),failed?Date.now():null,String(error.message).slice(0,500),row.org,row.id);}finally{db.close();}
|
|
388
|
+
pending.push({agent:row.id,error:error.message,attempts:made,failed});
|
|
389
|
+
}
|
|
367
390
|
}
|
|
368
391
|
return pending;
|
|
369
392
|
}
|
|
@@ -377,7 +400,7 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
|
377
400
|
db.exec('BEGIN IMMEDIATE');
|
|
378
401
|
db.prepare('INSERT INTO agent_runtime_sessions(org,runtime_ref,session_ref) VALUES (?,?,?) ON CONFLICT(org,runtime_ref) DO UPDATE SET session_ref=excluded.session_ref').run(owner.org,runtimeRef,session);
|
|
379
402
|
// A published native session remains history; an unpublished lease-only row is not a session.
|
|
380
|
-
db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=? AND published
|
|
403
|
+
db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=? AND published<=0').run(owner.org,runtimeRef);
|
|
381
404
|
db.exec('COMMIT');
|
|
382
405
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
383
406
|
return agentRunForSession(root,[runtimeRef]);
|
|
@@ -421,7 +444,13 @@ export function recordAgentRun(root,org,id,{session,machine=null,run,profile=nul
|
|
|
421
444
|
db.exec('COMMIT');
|
|
422
445
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
423
446
|
}
|
|
424
|
-
|
|
447
|
+
// A run waiting to be joined has published=0; a pass joining it holds published=-(the time it claimed it), by
|
|
448
|
+
// compare-and-set, and a claim older than SYNC_CLAIM_MS (a pass that died) is free again.
|
|
449
|
+
export function pendingAgentRuns(root,org){const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND (published=0 OR (published<0 AND -published<?)) ORDER BY rowid LIMIT 100').all(org,Date.now()-SYNC_CLAIM_MS).filter(row=>!(retryOf(root,org,row)?.next_at>Date.now()));}finally{db.close();}}
|
|
450
|
+
/** Claim one waiting run for this pass; false when another pass holds it. */
|
|
451
|
+
export function claimAgentRun(root,org,row){const db=store(root);try{return db.prepare('UPDATE agent_runs SET published=? WHERE org=? AND session_ref=? AND published=?').run(-Date.now(),org,row.session_ref,row.published).changes===1;}finally{db.close();}}
|
|
452
|
+
/** Give a claimed run back, unjoined. */
|
|
453
|
+
export function releaseAgentRun(root,org,row){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=0 WHERE org=? AND session_ref=? AND published<0').run(org,row.session_ref);}finally{db.close();}}
|
|
425
454
|
/** Session references come from the bound runtime, never from a model's requested agent id. */
|
|
426
455
|
export function agentRunForSession(root, references) {
|
|
427
456
|
const db = store(root);
|
|
@@ -468,7 +497,7 @@ export function runBudgetRefusal(root,agent,budgets,now){
|
|
|
468
497
|
return null;
|
|
469
498
|
}
|
|
470
499
|
/** A run whose session Teams never had and that no longer runs here can never be owned there: it leaves the sync. */
|
|
471
|
-
export function abandonAgentRun(root,org,session){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=2 WHERE org=? AND session_ref=? AND published
|
|
500
|
+
export function abandonAgentRun(root,org,session){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=2 WHERE org=? AND session_ref=? AND published<=0').run(org,session);db.prepare('DELETE FROM agent_run_retries WHERE org=? AND session_ref=?').run(org,session);}finally{db.close();}}
|
|
472
501
|
export function publishAgentRun(root,org,session){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=1 WHERE org=? AND session_ref=?').run(org,session);}finally{db.close();}}
|
|
473
502
|
export function agentPreamble(agent,{main=true,previous=[]}={}){
|
|
474
503
|
const {identity:a,intended,assignments=[]}=agent;
|
|
@@ -0,0 +1,29 @@
|
|
|
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 panes = await new StreamRpc(await channel.open('node')).call('harness.v1.panes.list', {});
|
|
16
|
+
const live = (Array.isArray(panes) ? panes : []).filter((pane) => !pane.dead && pane.pid);
|
|
17
|
+
let activities = [];
|
|
18
|
+
if (live.length) {
|
|
19
|
+
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
|
20
|
+
try { await client.start?.(); activities = (await client.request('harness.v1.sessions.activity_under', { pids: live.map((pane) => pane.pid) }))?.activities ?? []; }
|
|
21
|
+
finally { await client.close?.(); }
|
|
22
|
+
}
|
|
23
|
+
return { machine: name, panes, activities };
|
|
24
|
+
} finally { channel?.close(); }
|
|
25
|
+
}
|
|
26
|
+
const rows = await Promise.all(machines.map((machine) => within(read(machine), deadlineMs)
|
|
27
|
+
.catch((error) => ({ machine: machine.name, error: String(error?.message ?? error).slice(0, 300), code: error?.code ?? 'unavailable' }))));
|
|
28
|
+
process.stdout.write(`${JSON.stringify(rows)}\n`);
|
|
29
|
+
process.exit(0);
|
package/board/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath,
|
|
|
15
15
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
16
16
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
17
17
|
import * as engine from './engine.mjs';
|
|
18
|
-
import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick } from './dispatch.mjs';
|
|
18
|
+
import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick, deliver, startDelivery } from './dispatch.mjs';
|
|
19
19
|
import { watchBoard } from './wake.mjs';
|
|
20
20
|
import { streamBoardPublications } from './event-stream.mjs';
|
|
21
21
|
import { completionOwnerOf, isLatestReviewer, loadWorkflow, requestSessionClose, scopeOf, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
|
|
@@ -950,6 +950,14 @@ export async function runWorkflow(argv) {
|
|
|
950
950
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
951
951
|
return 0;
|
|
952
952
|
}
|
|
953
|
+
case 'deliver': {
|
|
954
|
+
// the delivery pass: due effects, board mail, subscription notices and the agent records' Teams sync (serve
|
|
955
|
+
// starts it after each round in a process of its own; never part of a round)
|
|
956
|
+
const reports = await deliver({ root, slugs: args.board ? [slug] : null });
|
|
957
|
+
if (args.json) say(args, '', reports);
|
|
958
|
+
else for (const r of reports) process.stdout.write(`${r.board}: ${r.error ? `error: ${r.error}` : `${r.effects_waiting ?? 0} effect(s) waiting, ${r.effects_failed ?? 0} failed for good, ${r.notified.length} notified`}\n`);
|
|
959
|
+
return reports.some((r) => r.error) ? 1 : 0;
|
|
960
|
+
}
|
|
953
961
|
case 'daemon': case 'serve': return serve(root, args);
|
|
954
962
|
|
|
955
963
|
case 'gc': {
|
|
@@ -1440,7 +1448,7 @@ async function serve(root, args = {}) {
|
|
|
1440
1448
|
const running = installed();
|
|
1441
1449
|
log(`dispatching the boards of ${root} from ${localMachine()} (orchestrator ${running ?? 'unknown'})`);
|
|
1442
1450
|
events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log });
|
|
1443
|
-
log('dispatch wakes on board writes, document edits
|
|
1451
|
+
log('dispatch wakes on board writes, document edits and board mail; the interval sweep reads each machine\'s own door');
|
|
1444
1452
|
const stuck = {};
|
|
1445
1453
|
const warned = {};
|
|
1446
1454
|
let pausedSaid = false;
|
|
@@ -1488,6 +1496,8 @@ async function serve(root, args = {}) {
|
|
|
1488
1496
|
interval = settings.interval;
|
|
1489
1497
|
const reports = await tick({ root, lanes: state.lanes, settings, params, deploymentReceipt: true });
|
|
1490
1498
|
round += 1;
|
|
1499
|
+
// what the round recorded is delivered by a pass of its own, which this loop never waits for
|
|
1500
|
+
if (!stopped) startDelivery(root, { log: (level, line) => { if (level === 'warn' || args.verbose) log(line); } });
|
|
1491
1501
|
if (!stopped) await watchRounds(reports);
|
|
1492
1502
|
for (const r of reports) {
|
|
1493
1503
|
nextMailAt = Math.min(nextMailAt, r.next_mail_at ?? Infinity);
|