@volter/supercode-orchestrator 0.5.35 → 0.5.37
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 +32 -10
- package/bin/agent-door.mjs +4 -1
- package/board/cli.mjs +7 -7
- package/board/dispatch.mjs +22 -33
- package/index.d.ts +2 -2
- package/package.json +1 -1
package/agents.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
8
8
|
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
9
9
|
import { resolveAgentProfile } from './agent-profiles.mjs';
|
|
10
10
|
import { compareAgentProfile } from '@volter/teams/agent-profile';
|
|
11
|
+
import { WorkspaceContexts } from '@volter/teams/context';
|
|
12
|
+
import { supercodeHome } from '@volter/teams/home';
|
|
11
13
|
const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
|
|
12
14
|
// The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
|
|
13
15
|
const HOST_CALLER=(process.env.SUPERCODE_CALLER_NAME??`orchestrator ${process.argv.slice(2).filter(arg=>!arg.startsWith('-')).slice(0,2).filter(arg=>/^[a-z][a-z0-9-]{0,30}$/.test(arg)).join(' ')}`).slice(0,80);
|
|
@@ -22,11 +24,9 @@ export function teamsUnavailable(error){
|
|
|
22
24
|
if(error?.status!=null)return error.status>=500||error.status===401;
|
|
23
25
|
return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
|
|
24
26
|
}
|
|
25
|
-
/** The Org agent identity belongs to: the selected Teams context
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
catch(error){if(teamsUnavailable(error))return {server:'local',org:'local',principal:null,offline:true,reason:error.message};throw error;}
|
|
29
|
-
}
|
|
27
|
+
/** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
|
|
28
|
+
* or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
|
|
29
|
+
export function agentScope(context){return agentDoor('scope',[],{context});}
|
|
30
30
|
function store(root){
|
|
31
31
|
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
|
|
32
32
|
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
@@ -260,27 +260,42 @@ export function ensureForkAgent(options) {
|
|
|
260
260
|
export function agentRunForKey(root,org,agent,key) {
|
|
261
261
|
const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND agent_id=? AND run_id=? ORDER BY rowid DESC LIMIT 1').get(org,agent,key)??null;}finally{db.close();}
|
|
262
262
|
}
|
|
263
|
+
const declarationHead=(root,org,key)=>{const db=store(root);try{return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,key)?.agent_id??null;}finally{db.close();}};
|
|
264
|
+
const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
|
|
263
265
|
function recordDeclaration(root,org,id,declaration){
|
|
264
266
|
const db=store(root);try{db.prepare('UPDATE agent_intents SET declaration_json=? WHERE org=? AND id=? AND declared=0').run(JSON.stringify(declaration),org,id);}finally{db.close();}
|
|
265
267
|
}
|
|
266
|
-
/**
|
|
267
|
-
*
|
|
268
|
+
/** The identity a run starts as without asking Teams: the daemon-minted id and retained intent. An identity not yet
|
|
269
|
+
* declared is declared later under the same id (synchronizeLocalAgents). Nothing is granted from this local record;
|
|
270
|
+
* `identity.synced` says whether Teams has the declaration, and the record's own `synced: false` that Teams was not read. */
|
|
268
271
|
function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
|
|
269
272
|
const {root,org,server,profiles,profile,machine=null}=options;
|
|
270
273
|
const rootProfile=profiles.default?'default':profile;
|
|
271
274
|
const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
272
275
|
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
|
|
273
|
-
|
|
276
|
+
let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
277
|
+
// An agent Teams ended is never started again: its end, once synchronized, is on its runs here (synchronizeAgentEnds),
|
|
278
|
+
// and the key's next generation is minted in its place, with fresh memory.
|
|
279
|
+
if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
|
|
274
280
|
if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
|
|
275
281
|
const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
|
|
276
282
|
const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
|
|
277
283
|
lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
|
|
278
|
-
synced
|
|
284
|
+
synced:!!intent.declared,main_session:null,revision:0};
|
|
279
285
|
const assigned=assignAgent(root,org,intent.id,assignment);
|
|
280
286
|
return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
|
|
281
287
|
}
|
|
282
288
|
export function localCardAgent(options){
|
|
283
|
-
const {card,role,profile}=options;
|
|
289
|
+
const {root,org,server,profiles,card,role,profile}=options;
|
|
290
|
+
// A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
|
|
291
|
+
// active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
|
|
292
|
+
const held=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
|
|
293
|
+
if(held&&held.agent_id!==declarationHead(root,org,`${server}/${org}/card/${card}/${role}`)&&!endedHere(root,org,held.agent_id)){
|
|
294
|
+
const record=localAgentRecord(root,org,held.agent_id);
|
|
295
|
+
const identity=record?{...record.identity,synced:true}:{id:held.agent_id,kind:'agent',org,name:held.agent_id,path:held.agent_id,parent:null,
|
|
296
|
+
lineage:{starter:null},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',synced:true,main_session:null,revision:0};
|
|
297
|
+
return {identity,intended:record?.intended??resolveAgentProfile(profiles,profile),assignment:held.id,assignments:[{kind:'card',target:card,role}],org,synced:false};
|
|
298
|
+
}
|
|
284
299
|
return localAgent(options,{key:`card/${card}/${role}`,name:`${card}-${role}`,assignment:{kind:'card',target:card,role},
|
|
285
300
|
declaration:{kind:'card',card,role,profile,sponsor:options.sponsor??null,starterSession:options.starterSession??null},
|
|
286
301
|
starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
|
|
@@ -459,6 +474,13 @@ export function agentPreamble(agent,{main=true,previous=[]}={}){
|
|
|
459
474
|
const {identity:a,intended,assignments=[]}=agent;
|
|
460
475
|
return `You are agent ${a.path} (${a.id}), ${main?'its main session':'a concurrent session'}.${a.synced===false?' This identity was minted on this machine and is not yet declared on a Teams server; it keeps this id when it is.':''}\nSponsor: ${a.sponsor??'none yet'}; parent: ${a.parent??'none'}; lineage: ${JSON.stringify(a.lineage)}.\nAssignments: ${assignments.map(value=>`${value.kind} ${value.target}, role ${value.role}`).join("; ")}; profile: ${a.profile}; permission layers: ${JSON.stringify(intended.resolved.permissions.layers)}.\nAsk again with supercode agent whoami. List your sessions with supercode agent sessions; read a transcript only when needed.${previous.length?'\nEarlier sessions:\n'+previous.map(s=>`${s.session} · ${s.machine??'unknown machine'} · ended ${s.ended_at??'live'} · ${s.end_reason??''}`).join('\n'):''}`;
|
|
461
476
|
}
|
|
477
|
+
/** The agent's reader context this home already holds (a session of it was bound here before), read from the contexts
|
|
478
|
+
* file without a request, or null. A launch on this machine hands it to the session, so its Teams verbs run delegated
|
|
479
|
+
* from the first turn; a context that has since expired fails there, as an absent one does. */
|
|
480
|
+
export function localReaderContext(org,id){
|
|
481
|
+
const contexts=new WorkspaceContexts(supercodeHome()).read().contexts??{};
|
|
482
|
+
return Object.entries(contexts).find(([,value])=>value?.principal_id===id&&value?.team_id===org)?.[0]??null;
|
|
483
|
+
}
|
|
462
484
|
export function agentProfileMismatch(intent,resolved){return compareAgentProfile(intent?.resolved??intent,resolved);}
|
|
463
485
|
|
|
464
486
|
export function agentEnvironment(agent,{root,context,session=null,main=true}={}) {
|
package/bin/agent-door.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The orchestrator's synchronous host reaches Teams through the installed neutral SDK.
|
|
3
|
-
import { fromContext } from '@volter/teams/context';
|
|
3
|
+
import { fromContext, WorkspaceContexts } from '@volter/teams/context';
|
|
4
4
|
import { supercodeHome } from '@volter/teams/home';
|
|
5
5
|
import { openMachine, normalMachineName } from '@volter/teams/machine-door';
|
|
6
6
|
import { hostname } from 'node:os';
|
|
@@ -11,6 +11,9 @@ const reach=machine=>machine&&normalMachineName(machine)!==normalMachineName(hos
|
|
|
11
11
|
let input='';for await(const chunk of process.stdin)input+=chunk;
|
|
12
12
|
try{
|
|
13
13
|
const {op,args=[],context}=JSON.parse(input);
|
|
14
|
+
// No context selected: identity is this home's own (Org `local`). A context that is named but unreadable is an error.
|
|
15
|
+
const named=context??process.env.SUPERCODE_CONTEXT??new WorkspaceContexts(supercodeHome()).read().current;
|
|
16
|
+
if(op==='scope'&&(!named||named==='local')){process.stdout.write(JSON.stringify({server:'local',org:'local',principal:null,offline:true,reason:'no Teams context is selected'})+'\n');process.exit(0);}
|
|
14
17
|
const {client,context:scope}=fromContext({supercodeHome:supercodeHome(),name:context??process.env.SUPERCODE_CONTEXT});
|
|
15
18
|
const team=client.team(scope.team_id),options={signal:AbortSignal.timeout(20000)};
|
|
16
19
|
let result;
|
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, 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 } 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';
|
|
@@ -1379,9 +1379,9 @@ async function serve(root, args = {}) {
|
|
|
1379
1379
|
if (args.pidfile) writeFileSync(args.pidfile, `${process.pid}\n`);
|
|
1380
1380
|
let stopped = false;
|
|
1381
1381
|
let events;
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1382
|
+
// a signal ends the round in progress with nothing new started (dispatch.mjs stopDispatching) and no round after it
|
|
1383
|
+
const stop = () => { stopped = true; stopDispatching(); events?.close(); };
|
|
1384
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, stop);
|
|
1385
1385
|
// what the serve says is kept in the home too (logs/workflow-serve.log), so why it stopped outlives its pane
|
|
1386
1386
|
const kept = join(root, 'logs', 'workflow-serve.log');
|
|
1387
1387
|
const log = (text) => {
|
|
@@ -1488,7 +1488,7 @@ async function serve(root, args = {}) {
|
|
|
1488
1488
|
interval = settings.interval;
|
|
1489
1489
|
const reports = await tick({ root, lanes: state.lanes, settings, params, deploymentReceipt: true });
|
|
1490
1490
|
round += 1;
|
|
1491
|
-
await watchRounds(reports);
|
|
1491
|
+
if (!stopped) await watchRounds(reports);
|
|
1492
1492
|
for (const r of reports) {
|
|
1493
1493
|
nextMailAt = Math.min(nextMailAt, r.next_mail_at ?? Infinity);
|
|
1494
1494
|
const said = summary(r);
|
|
@@ -1509,7 +1509,7 @@ async function serve(root, args = {}) {
|
|
|
1509
1509
|
}
|
|
1510
1510
|
} catch (error) {
|
|
1511
1511
|
log(`tick failed: ${error.message}`);
|
|
1512
|
-
await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
|
|
1512
|
+
if (!stopped) await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
|
|
1513
1513
|
}
|
|
1514
1514
|
// each round is a line of its own in the slow log, with the steps it spent its time on
|
|
1515
1515
|
recordSlow({ kind: 'call', name: 'dispatch round', ms: Date.now() - began, slowest: null, extra: { steps: takeSteps() } });
|
|
@@ -1529,7 +1529,7 @@ async function serve(root, args = {}) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
} finally {
|
|
1531
1531
|
events?.close();
|
|
1532
|
-
|
|
1532
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.off(signal, stop);
|
|
1533
1533
|
if (args.pidfile) { try { unlinkSync(args.pidfile); } catch { /* gone */ } }
|
|
1534
1534
|
}
|
|
1535
1535
|
log(stopped ? 'stopped (a signal)' : 'stopped');
|
package/board/dispatch.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { readEventFleet } from './event-fleet.mjs';
|
|
|
20
20
|
let eventFleetRoot = null;
|
|
21
21
|
import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
22
22
|
import { runtimeDoor } from './runtime-door.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
|
|
24
24
|
import { materializeAgentHome } from '../agent-home.mjs';
|
|
25
25
|
import { memorySnapshotSync } from '../hermes-cli.mjs';
|
|
26
26
|
import { createRequire } from 'node:module';
|
|
@@ -684,28 +684,20 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
684
684
|
// the model it runs is recorded with the session, for caps that count by model
|
|
685
685
|
const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
|
|
686
686
|
try {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if(!team.offline){
|
|
693
|
-
try{
|
|
694
|
-
agent=ensureCardAgent(cardOptions);
|
|
695
|
-
previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
|
|
696
|
-
reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
|
|
697
|
-
}catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
|
|
698
|
-
}
|
|
699
|
-
agent??=localCardAgent(cardOptions);
|
|
687
|
+
// Teams is never on a launch's path: the card's agent is its identity minted on this host under its retained intent,
|
|
688
|
+
// and its sessions are joined to it off this path, after the launch. No Teams answer starts, stops or holds a card.
|
|
689
|
+
const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context);
|
|
690
|
+
const agent=localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
|
|
691
|
+
profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
|
|
700
692
|
// G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
|
|
701
693
|
const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
|
|
702
694
|
const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
|
|
703
695
|
if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
|
|
704
|
-
previous
|
|
696
|
+
const previous=localAgentSessions(board.root,team.org,agent.identity.id);
|
|
705
697
|
prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
|
|
706
698
|
Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
|
|
707
699
|
agent_profile_intent:structuredClone(agent.intended.resolved),
|
|
708
|
-
agent_env:agentEnvironment(agent,{root:board.root,context:
|
|
700
|
+
agent_env:agentEnvironment(agent,{root:board.root,context:normalMachine(task.machine??localMachine())===localMachine()?localReaderContext(team.org,agent.identity.id)??undefined:undefined})});
|
|
709
701
|
// The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
|
|
710
702
|
// another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
|
|
711
703
|
const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
|
|
@@ -718,14 +710,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
718
710
|
setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
|
|
719
711
|
event(db,id,'restart_authorization_consumed',{},runId);
|
|
720
712
|
});
|
|
721
|
-
if(result?.session_id){
|
|
722
|
-
const ref=`${result.harness}:${result.session_id}`;
|
|
723
|
-
recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
|
|
724
|
-
if(agent.synced!==false){
|
|
725
|
-
try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
|
|
726
|
-
catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
713
|
+
if(result?.session_id)recordAgentRun(board.root,team.org,agent.identity.id,{session:`${result.harness}:${result.session_id}`,machine:result.machine,run:runId});
|
|
729
714
|
return result;
|
|
730
715
|
} catch(error) {
|
|
731
716
|
// An issued asynchronous launch may bind later. Its original permission
|
|
@@ -1831,7 +1816,7 @@ function adoptSession(board, id, who, { request = null, open = request, progress
|
|
|
1831
1816
|
const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
|
|
1832
1817
|
const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
|
|
1833
1818
|
const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
|
|
1834
|
-
// The request is claimed before this adoption's first write outside the board (the agent
|
|
1819
|
+
// The request is claimed before this adoption's first write outside the board (the agent store, below), in
|
|
1835
1820
|
// a transaction that finds it still open and the card's run the one read: a request answered or claimed meanwhile, or
|
|
1836
1821
|
// a run that changed, ends it here with nothing written anywhere, and no other round acts on a request claimed here.
|
|
1837
1822
|
const stale = (db) => {
|
|
@@ -1845,11 +1830,8 @@ function adoptSession(board, id, who, { request = null, open = request, progress
|
|
|
1845
1830
|
event(db, id, 'adoption_claimed', { request, session: who, ...newClaim(ttl) });
|
|
1846
1831
|
});
|
|
1847
1832
|
progress.claimed = request !== null;
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
|
|
1851
|
-
catch (error) { if (!teamsUnavailable(error)) throw error; }
|
|
1852
|
-
}
|
|
1833
|
+
// The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
|
|
1834
|
+
const agent = lane ? localCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null }) : null;
|
|
1853
1835
|
// A session another agent already owns is refused before anything is recorded: the run would name one agent and the
|
|
1854
1836
|
// agent store another, and every round after would trip on it.
|
|
1855
1837
|
const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
|
|
@@ -2009,6 +1991,7 @@ export async function sendBoardMessages(board, report, taskId = null) {
|
|
|
2009
1991
|
const planned = tx(board, db => pendingRequestMail(db, taskId));
|
|
2010
1992
|
if (Number.isFinite(planned.nextAt)) report.next_mail_at = Math.min(report.next_mail_at ?? Infinity, planned.nextAt);
|
|
2011
1993
|
for (const mail of planned.ready) {
|
|
1994
|
+
if (stopping) break;
|
|
2012
1995
|
const first = mail.members[0];
|
|
2013
1996
|
try {
|
|
2014
1997
|
const task = view(board, db => getTask(db, first.task_id));
|
|
@@ -2189,7 +2172,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
2189
2172
|
} else text = noticeOf(entry, role, ev.kind, task, p);
|
|
2190
2173
|
if (text) {
|
|
2191
2174
|
const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
|
|
2192
|
-
if (refused.has(sub.chat_id)) { untried = true; break; }
|
|
2175
|
+
if (refused.has(sub.chat_id) || stopping) { untried = true; break; }
|
|
2193
2176
|
let receipt;
|
|
2194
2177
|
try {
|
|
2195
2178
|
receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: String(sub.chat_id).includes(':claude-code:') ? false : !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
|
|
@@ -2454,6 +2437,11 @@ function scAsync(args, { input = null, timeout = 120_000 } = {}) {
|
|
|
2454
2437
|
});
|
|
2455
2438
|
}
|
|
2456
2439
|
|
|
2440
|
+
// A dispatcher told to stop (its serve's SIGTERM, SIGINT or SIGHUP) starts nothing more in the round it is in: no further
|
|
2441
|
+
// board, card start, board message or notice. What it leaves untried is not a failure; the next dispatcher sends it.
|
|
2442
|
+
let stopping = false;
|
|
2443
|
+
export function stopDispatching() { stopping = true; }
|
|
2444
|
+
|
|
2457
2445
|
// how many deliveries a tick has in flight at once (each is a `supercode message` process of a few seconds)
|
|
2458
2446
|
const SEND_CONCURRENCY = 8;
|
|
2459
2447
|
|
|
@@ -2461,7 +2449,7 @@ const SEND_CONCURRENCY = 8;
|
|
|
2461
2449
|
async function pool(items, n, fn) {
|
|
2462
2450
|
let next = 0;
|
|
2463
2451
|
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
2464
|
-
while (next < items.length) { const i = next; next += 1; await fn(items[i]); }
|
|
2452
|
+
while (next < items.length && !stopping) { const i = next; next += 1; await fn(items[i]); }
|
|
2465
2453
|
}));
|
|
2466
2454
|
}
|
|
2467
2455
|
|
|
@@ -2598,6 +2586,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2598
2586
|
const held = dispatch ? homeHoldings(root, boards, lanes, fleet, entry) : [];
|
|
2599
2587
|
const out = [];
|
|
2600
2588
|
for (const slug of boards) {
|
|
2589
|
+
if (stopping) break;
|
|
2601
2590
|
const report = { board: slug, ms: { fleet: fleetMs }, mailed: [], started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
|
|
2602
2591
|
// the mailbox's answers in flight (not part of the report the caller prints)
|
|
2603
2592
|
Object.defineProperty(report, 'answers', { value: [], enumerable: false });
|
|
@@ -2628,7 +2617,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2628
2617
|
tx(board,db=>auditEndedRuns(db,board,entry));
|
|
2629
2618
|
if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
|
|
2630
2619
|
await sendBoardMessages(board, report);
|
|
2631
|
-
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|
|
2620
|
+
if (dispatch && !stopping) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|
|
2632
2621
|
phase('start');
|
|
2633
2622
|
if (dispatch && !only && !report.paused) observeIdle(board, fleet, settings, report, entry, effects);
|
|
2634
2623
|
// what the moves asked for, once they are committed (messages to actors, closed panes, cleaned workspaces)
|
package/index.d.ts
CHANGED
|
@@ -128,10 +128,10 @@ export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: str
|
|
|
128
128
|
export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
|
|
129
129
|
export function agentPreamble(agent: ReturnType<typeof ensureCardAgent>, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
|
|
130
130
|
export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
|
|
131
|
-
/** The Org for agent identity; `offline` when
|
|
131
|
+
/** The Org for agent identity, read from this home's selected context without a request; `offline` when none is selected. Throws when a named context cannot be read. */
|
|
132
132
|
export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
|
|
133
133
|
export function teamsUnavailable(error: unknown): boolean;
|
|
134
|
-
/**
|
|
134
|
+
/** The identity a run starts as without asking Teams; `identity.synced` is false until Teams has its declaration, made later under the same id. */
|
|
135
135
|
export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
136
136
|
export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
137
137
|
export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.37",
|
|
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": {
|