@volter/supercode-orchestrator 0.5.42 → 0.5.44
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/activation.mjs +19 -0
- package/agents.mjs +63 -237
- package/bin/agent-door.mjs +0 -17
- package/board/agent-declarations.mjs +20 -7
- package/board/dispatch.mjs +14 -33
- package/board/engine.mjs +3 -0
- package/index.d.ts +10 -15
- package/index.mjs +1 -1
- package/kanban-worker.mjs +2 -1
- package/loop.mjs +26 -62
- package/package.json +2 -2
package/activation.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { mkdirSync, readFileSync, readdirSync, lstatSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
8
|
import { CLAUDE_BRIDGE, linkTo, skillSources } from './hermes-home.mjs';
|
|
9
9
|
import { ownLogin } from './worker-route.mjs';
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
10
11
|
|
|
11
12
|
/** Which env var each harness reads its config home from (supercode registry evidence). */
|
|
12
13
|
export const HOME_ENV = Object.freeze({
|
|
@@ -54,6 +55,24 @@ export function activationEnv({ root, profile, surfaceKey, vault }) {
|
|
|
54
55
|
return env;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const declaredHomes = new Set();
|
|
59
|
+
/**
|
|
60
|
+
* A worker that keeps its sessions in a config home of its own (its profile's CODEX_HOME or CLAUDE_CONFIG_DIR) is
|
|
61
|
+
* declared to this machine's connector through its own door (`supercode teams roots home add`), on each root covering
|
|
62
|
+
* the folder it runs in, so its sessions are found and published (D138 step 5). Once per home and folder per process,
|
|
63
|
+
* off the launch's path: the door is local, and a refusal (no connected root covers the folder) is only said.
|
|
64
|
+
*/
|
|
65
|
+
export function declareWorkerHome({ harness, env, cwd, log = () => {} }) {
|
|
66
|
+
const home = ['codex', 'claude-code'].includes(harness) ? env?.[HOME_ENV[harness]] : null;
|
|
67
|
+
if (!home || declaredHomes.has(`${harness}\0${home}\0${cwd}`)) return;
|
|
68
|
+
declaredHomes.add(`${harness}\0${home}\0${cwd}`);
|
|
69
|
+
execFile(process.env.SUPERCODE_BIN || 'supercode', ['teams', 'roots', 'home', 'add', home, '--harness', harness, '--cwd', cwd, '--json'], { timeout: 30_000 }, (error, _stdout, stderr) => {
|
|
70
|
+
if (!error) return;
|
|
71
|
+
declaredHomes.delete(`${harness}\0${home}\0${cwd}`);
|
|
72
|
+
log('warn', `worker home ${home} not declared to this machine's connector: ${String(stderr || error.message).trim().split('\n').at(-1)}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
export function activationCwd(profile, workdir) {
|
|
58
77
|
return resolve(profile.dir, workdir ?? profile.worker?.cwd ?? '.');
|
|
59
78
|
}
|
package/agents.mjs
CHANGED
|
@@ -13,17 +13,14 @@ import { supercodeHome } from '@volter/teams/home';
|
|
|
13
13
|
const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
|
|
14
14
|
// The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
|
|
15
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);
|
|
16
|
+
const lastJson=text=>{for(const line of String(text??'').trim().split('\n').reverse()){try{const value=JSON.parse(line);if(value&&typeof value==='object')return value;}catch{/* not the door's line */}}return null;};
|
|
16
17
|
export function agentDoor(op,args=[],{context,timeout=25000}={}){
|
|
17
18
|
const reply=spawnSync(process.execPath,[entry],{input:JSON.stringify({op,args,context}),encoding:'utf8',timeout,maxBuffer:4*1024*1024,env:{...process.env,SUPERCODE_CALLER_NAME:`${HOST_CALLER} agent-door ${op}`}});
|
|
18
|
-
|
|
19
|
+
// The door's answer to a failure is the last stderr line that is JSON: a runtime's own lines (a preload's notice, a
|
|
20
|
+
// Node warning) may come before or after it.
|
|
21
|
+
if(reply.status!==0){const detail=lastJson(reply.stderr)??{message:reply.error?.message??String(reply.stderr).slice(-1000)};throw Object.assign(new Error(detail.message??'Teams agent door unavailable'),detail);}
|
|
19
22
|
return JSON.parse(reply.stdout);
|
|
20
23
|
}
|
|
21
|
-
/** Teams is not configured, not reachable, or no longer accepts this login: identity stays local until it syncs.
|
|
22
|
-
* A refusal Teams decided (forbidden, conflict, not found, invalid) is never read as absence. */
|
|
23
|
-
export function teamsUnavailable(error){
|
|
24
|
-
if(error?.status!=null)return error.status>=500||error.status===401;
|
|
25
|
-
return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
|
|
26
|
-
}
|
|
27
24
|
/** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
|
|
28
25
|
* or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
|
|
29
26
|
export function agentScope(context){return agentDoor('scope',[],{context});}
|
|
@@ -31,18 +28,12 @@ function store(root){
|
|
|
31
28
|
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
|
|
32
29
|
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
33
30
|
CREATE TABLE IF NOT EXISTS agent_intents(org TEXT NOT NULL,id TEXT NOT NULL,key TEXT NOT NULL,profile_ref TEXT NOT NULL,profile_json TEXT NOT NULL,declared INTEGER NOT NULL DEFAULT 0,declaration_json TEXT,created_at INTEGER NOT NULL,PRIMARY KEY(org,id),UNIQUE(org,key));
|
|
34
|
-
CREATE TABLE IF NOT EXISTS agent_run_retries(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,next_at INTEGER NOT NULL,wait_ms INTEGER NOT NULL,PRIMARY KEY(org,agent_id,session_ref));
|
|
35
31
|
CREATE TABLE IF NOT EXISTS agent_declaration_heads(org TEXT NOT NULL,key TEXT NOT NULL,agent_id TEXT NOT NULL,generation INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(org,key));
|
|
36
32
|
CREATE TABLE IF NOT EXISTS agent_assignments(org TEXT NOT NULL,id TEXT NOT NULL,agent_id TEXT NOT NULL,kind TEXT NOT NULL,target TEXT NOT NULL,role TEXT NOT NULL,state TEXT NOT NULL,started_at INTEGER NOT NULL,ended_at INTEGER,PRIMARY KEY(org,id));
|
|
37
33
|
CREATE INDEX IF NOT EXISTS agent_active_assignments ON agent_assignments(org,agent_id,state);
|
|
38
34
|
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
35
|
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
|
-
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,
|
|
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');
|
|
36
|
+
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,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));`);
|
|
46
37
|
return db;
|
|
47
38
|
}
|
|
48
39
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
@@ -86,25 +77,6 @@ export function finishAgentAssignment(root,org,assignment,reason){
|
|
|
86
77
|
db.exec('COMMIT');
|
|
87
78
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
88
79
|
}
|
|
89
|
-
export function synchronizeAgentEnds(root,org,{context}={}){
|
|
90
|
-
const db=store(root),pending=[];
|
|
91
|
-
try{
|
|
92
|
-
for(const row of db.prepare('SELECT * FROM agent_end_outbox WHERE org=? ORDER BY created_at LIMIT 100').all(org)){
|
|
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;}
|
|
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;
|
|
98
|
-
try{
|
|
99
|
-
agentDoor('end',[row.agent_id,row.reason,digest(`${org}/${row.agent_id}/end/${row.created_at}`)],{context});
|
|
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);
|
|
101
|
-
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,row.agent_id);
|
|
102
|
-
}
|
|
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});}
|
|
104
|
-
}
|
|
105
|
-
return pending;
|
|
106
|
-
}finally{db.close();}
|
|
107
|
-
}
|
|
108
80
|
/** Mint and retain intent before any network call. Offline declarations keep their immutable id for sync. */
|
|
109
81
|
export function declareAgentIntent({root,org,server,profiles,profile,key,machine=null,refresh=true}) {
|
|
110
82
|
const intended=resolveAgentProfile(profiles,profile),stable=`${server}/${org}/${key}`,db=store(root);
|
|
@@ -122,16 +94,6 @@ export function declareAgentIntent({root,org,server,profiles,profile,key,machine
|
|
|
122
94
|
return {id:row.id,key:row.key,baseKey:stable,declared:!!row.declared,intended:refresh?intended:JSON.parse(row.profile_json),fresh,org};
|
|
123
95
|
}finally{db.close();}
|
|
124
96
|
}
|
|
125
|
-
function adoptAgentIntent(root,intent,identity){
|
|
126
|
-
const db=store(root);try{
|
|
127
|
-
db.exec('BEGIN IMMEDIATE');
|
|
128
|
-
const existing=db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(intent.org,identity.id);
|
|
129
|
-
if(!existing)db.prepare('INSERT INTO agent_intents(org,id,key,profile_ref,profile_json,declared,created_at) VALUES (?,?,?,?,?,1,?)').run(intent.org,identity.id,`${intent.baseKey}/adopted/${identity.id}`,identity.profile,JSON.stringify(intent.intended),Date.now());
|
|
130
|
-
db.prepare('UPDATE agent_declaration_heads SET agent_id=? WHERE org=? AND key=?').run(identity.id,intent.org,intent.baseKey);
|
|
131
|
-
db.exec('COMMIT');
|
|
132
|
-
return {...intent,id:identity.id,key:existing?.key??`${intent.baseKey}/adopted/${identity.id}`,declared:true};
|
|
133
|
-
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
134
|
-
}
|
|
135
97
|
function nextAgentGeneration(root,intent,machine){
|
|
136
98
|
const db=store(root);try{
|
|
137
99
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -146,125 +108,6 @@ function nextAgentGeneration(root,intent,machine){
|
|
|
146
108
|
db.exec('COMMIT');return {...intent,id,key,declared:false};
|
|
147
109
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
148
110
|
}
|
|
149
|
-
function synchronizeIntent(intent,{root,context,body}) {
|
|
150
|
-
let identity;
|
|
151
|
-
if(intent.declared)identity=agentDoor('get',[intent.id],{context}).data;
|
|
152
|
-
else {
|
|
153
|
-
try{identity=agentDoor('declare',[{...body,id:intent.id},digest(intent.key)],{context}).data;}
|
|
154
|
-
catch(error){if(error.code==='idempotency_conflict')identity=agentDoor('get',[intent.id],{context}).data;else throw error;}
|
|
155
|
-
identity=agentDoor('get',[intent.id],{context}).data;
|
|
156
|
-
const db=store(root);try{db.prepare('UPDATE agent_intents SET declared=1 WHERE org=? AND id=?').run(intent.org,intent.id);}finally{db.close();}
|
|
157
|
-
}
|
|
158
|
-
if(identity.state!=='active')throw new Error(`agent ${identity.path} is ${identity.state}: ${identity.suspension_reason??identity.end_reason??'unavailable'}`);
|
|
159
|
-
if(body.profile!==identity.profile)identity=agentDoor('profile',[identity.id,body.profile,identity.revision],{context}).data;
|
|
160
|
-
if(body.parent!==undefined&&body.parent!==identity.parent)identity=agentDoor('reparent',[identity.id,body.parent,identity.revision],{context}).data;
|
|
161
|
-
return identity;
|
|
162
|
-
}
|
|
163
|
-
export function ensureRootAgent(options) {
|
|
164
|
-
const {root,org,context,profile=options.profiles.default?'default':null}=options;
|
|
165
|
-
if(!profile)throw new Error('declare the Org root with a named profile before launching card agents');
|
|
166
|
-
let intent=declareAgentIntent({...options,profile,key:'root'});
|
|
167
|
-
let identity;
|
|
168
|
-
try{identity=agentDoor('root',[],{context}).data;}
|
|
169
|
-
catch(error){
|
|
170
|
-
if(error.status!==404)throw error;
|
|
171
|
-
try{identity=synchronizeIntent(intent,{root,context,body:{name:basename(root),profile,root:true}});}
|
|
172
|
-
catch(conflict){if(conflict.status!==409)throw conflict;identity=agentDoor('root',[],{context}).data;}
|
|
173
|
-
}
|
|
174
|
-
if(identity.state==='ended'){
|
|
175
|
-
if(intent.id!==identity.id)intent=adoptAgentIntent(root,intent,identity);
|
|
176
|
-
intent=nextAgentGeneration(root,intent,options.machine);
|
|
177
|
-
identity=synchronizeIntent(intent,{root,context,body:{name:identity.name,profile,root:true}});
|
|
178
|
-
}
|
|
179
|
-
if(intent.id!==identity.id)adoptAgentIntent(root,intent,identity);
|
|
180
|
-
// Another admitted manager may have declared the root first; its server id remains authoritative.
|
|
181
|
-
if(identity.state!=='active')throw new Error('the Org root agent is not active');
|
|
182
|
-
return identity;
|
|
183
|
-
}
|
|
184
|
-
/** Cold starts and relaunches use the same local intent and public declaration. Reviewer and implementer keys differ. */
|
|
185
|
-
export function ensureCardAgent(options) {
|
|
186
|
-
const {root,org,profiles,profile,card,role,sponsor=null,context}=options;
|
|
187
|
-
const assigned=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
|
|
188
|
-
// A locally minted agent that has not reached Teams yet is declared below under the same key and id.
|
|
189
|
-
if(assigned&&agentIntent(root,org,assigned.agent_id)?.declared!==0) {
|
|
190
|
-
const identity=agentDoor('get',[assigned.agent_id],{context}).data;
|
|
191
|
-
if(identity.state==='active'){
|
|
192
|
-
const intended=resolveAgentProfile(profiles,identity.profile);
|
|
193
|
-
const db=store(root);try{db.prepare('UPDATE agent_intents SET profile_ref=?,profile_json=? WHERE org=? AND id=?').run(identity.profile,JSON.stringify(intended),org,identity.id);}finally{db.close();}
|
|
194
|
-
return {identity,intended,assignment:assigned.id,
|
|
195
|
-
assignments:activeAgentAssignments(root).filter(item=>item.org===org&&item.agent_id===identity.id),org};
|
|
196
|
-
}
|
|
197
|
-
if(identity.state!=='ended')throw new Error(`card's assigned agent is ${identity.state}`);
|
|
198
|
-
finishAgentAssignment(root,org,assigned.id,`agent ${identity.id} is ended`);
|
|
199
|
-
}
|
|
200
|
-
let intent=declareAgentIntent({...options,key:`card/${card}/${role}`});
|
|
201
|
-
const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
|
|
202
|
-
if(!intent.declared){
|
|
203
|
-
const existing=agentDoor('find',[`${card}-${role}`,rootAgent.id],{context}).data;
|
|
204
|
-
if(existing){
|
|
205
|
-
intent=adoptAgentIntent(root,intent,existing);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if(intent.declared&&agentDoor('get',[intent.id],{context}).data.state==='ended')intent=nextAgentGeneration(root,intent,options.machine);
|
|
209
|
-
const identity=synchronizeIntent(intent,{root,context,body:{name:`${card}-${role}`,parent:rootAgent.id,sponsor,profile,...(options.starterSession?{starter_session:options.starterSession}:{})}});
|
|
210
|
-
const assignment=assignAgent(root,org,identity.id,{kind:'card',target:card,role});
|
|
211
|
-
return {identity,intended:intent.intended,assignment,assignments:[{kind:'card',target:card,role}],org};
|
|
212
|
-
}
|
|
213
|
-
/** Starting a named profile declares its standing agent; bindings are sessions of that identity. */
|
|
214
|
-
export function ensureProfileAgent(options){
|
|
215
|
-
const {root,org,profiles,profile,context,agent:declared=null,declarations={}}=options;
|
|
216
|
-
if(declared&&declared!==profile){
|
|
217
|
-
// A declared agent (the IR's `agents`) runs as a shared profile under its own identity and parent.
|
|
218
|
-
const decl=declarations[declared]??{};
|
|
219
|
-
const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
|
|
220
|
-
const parent=decl.parent?ensureProfileAgent({...options,profile:declarations[decl.parent]?.profile??decl.parent,agent:decl.parent}).identity:rootAgent;
|
|
221
|
-
let declaration=declareAgentIntent({...options,key:`agent/${declared}`});
|
|
222
|
-
if(!declaration.declared){
|
|
223
|
-
const existing=agentDoor('find',[declared,parent.id],{context}).data;
|
|
224
|
-
if(existing)declaration=adoptAgentIntent(root,declaration,existing);
|
|
225
|
-
}
|
|
226
|
-
if(declaration.declared&&agentDoor('get',[declaration.id],{context}).data.state==='ended')declaration=nextAgentGeneration(root,declaration,options.machine);
|
|
227
|
-
const identity=synchronizeIntent(declaration,{root,context,body:{name:declared,parent:parent.id,profile,...(decl.sponsor?{sponsor:decl.sponsor}:{})}});
|
|
228
|
-
const assignment=assignAgent(root,org,identity.id,{kind:'agent',target:declared,role:'declared'});
|
|
229
|
-
return {identity,intended:declaration.intended,assignment,assignments:[{kind:'agent',target:declared,role:'declared'}],org};
|
|
230
|
-
}
|
|
231
|
-
const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
|
|
232
|
-
let identity=rootAgent,intent;
|
|
233
|
-
if(profile===(profiles.default?'default':profile))intent=agentIntent(root,org,identity.id)?.profile;
|
|
234
|
-
else{
|
|
235
|
-
// Profile ancestry and agent parentage are the same intended narrowing tree.
|
|
236
|
-
// Resolve first to reject a cycle before declaring any parent along it.
|
|
237
|
-
resolveAgentProfile(profiles,profile);
|
|
238
|
-
const parentProfile=profiles[profile].parent;
|
|
239
|
-
const parent=parentProfile?ensureProfileAgent({...options,profile:parentProfile}).identity:rootAgent;
|
|
240
|
-
let declaration=declareAgentIntent({...options,key:`profile/${profile}`});
|
|
241
|
-
if(!declaration.declared){
|
|
242
|
-
const existing=agentDoor('find',[profile,parent.id],{context}).data;
|
|
243
|
-
if(existing)declaration=adoptAgentIntent(root,declaration,existing);
|
|
244
|
-
}
|
|
245
|
-
if(declaration.declared&&agentDoor('get',[declaration.id],{context}).data.state==='ended')declaration=nextAgentGeneration(root,declaration,options.machine);
|
|
246
|
-
identity=synchronizeIntent(declaration,{root,context,body:{name:profile,parent:parent.id,profile}});
|
|
247
|
-
intent=declaration.intended;
|
|
248
|
-
}
|
|
249
|
-
const assignment=assignAgent(root,org,identity.id,{kind:'profile',target:profile,role:'profile'});
|
|
250
|
-
return {identity,intended:intent??resolveAgentProfile(profiles,profile),assignment,assignments:[{kind:'profile',target:profile,role:'profile'}],org};
|
|
251
|
-
}
|
|
252
|
-
/** An explicit fork declares a child; a routine helper launch keeps its starter's identity. */
|
|
253
|
-
export function ensureForkAgent(options) {
|
|
254
|
-
const {root,org,profiles,parent,session,name,key,context}=options;
|
|
255
|
-
const managed=agentDoor('management',[parent],{context}).data;
|
|
256
|
-
const source=agentDoor('session',[session],{context}).data;
|
|
257
|
-
if(source.agent?.id!==parent)throw new Error('fork source is not owned by the declared parent');
|
|
258
|
-
const parentIntent=agentIntent(root,org,parent)?.profile??resolveAgentProfile(profiles,managed.profile);
|
|
259
|
-
let intent=declareAgentIntent({...options,profile:managed.profile,key:`fork/${parent}/${key}`,refresh:false});
|
|
260
|
-
if(intent.declared&&agentDoor('get',[intent.id],{context}).data.state==='ended')throw new Error('this fork identity ended; declare a new fork key');
|
|
261
|
-
// The snapshot copies configuration only. Memory stays the new child's own empty state.
|
|
262
|
-
const intended=structuredClone(intent.fresh?parentIntent:intent.intended);
|
|
263
|
-
const db=store(root);try{db.prepare('UPDATE agent_intents SET profile_json=? WHERE org=? AND id=?').run(JSON.stringify(intended),org,intent.id);}finally{db.close();}
|
|
264
|
-
const identity=synchronizeIntent(intent,{root,context,body:{name,parent,profile:managed.profile,starter_session:session}});
|
|
265
|
-
const assignment=assignAgent(root,org,identity.id,{kind:'fork',target:session,role:key});
|
|
266
|
-
return {identity,intended,assignment,assignments:[{kind:'fork',target:session,role:key}],org};
|
|
267
|
-
}
|
|
268
111
|
export function agentRunForKey(root,org,agent,key) {
|
|
269
112
|
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();}
|
|
270
113
|
}
|
|
@@ -273,20 +116,20 @@ const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SE
|
|
|
273
116
|
function recordDeclaration(root,org,id,declaration){
|
|
274
117
|
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();}
|
|
275
118
|
}
|
|
276
|
-
/** The identity a run starts as
|
|
277
|
-
*
|
|
278
|
-
* `identity.synced` says whether
|
|
119
|
+
/** The identity a run starts as, never asking Teams: the daemon-minted id and retained intent. Its declaration is handed
|
|
120
|
+
* to this machine's daemon (handOffAgents), which declares it to Teams under the same id. Nothing is granted from this
|
|
121
|
+
* local record; `identity.synced` says whether the declaration has been handed on. */
|
|
279
122
|
function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
|
|
280
123
|
const {root,org,server,profiles,profile,machine=null}=options;
|
|
281
124
|
const rootProfile=profiles.default?'default':profile;
|
|
282
125
|
const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
283
|
-
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
|
|
126
|
+
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile,name:basename(root),parent:null});
|
|
284
127
|
let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
285
|
-
// An agent
|
|
286
|
-
//
|
|
128
|
+
// An agent ended here is never started again: its end is on its runs here (handOffAgents), and the key's next generation
|
|
129
|
+
// is minted in its place, with fresh memory.
|
|
287
130
|
if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
|
|
288
|
-
if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
|
|
289
131
|
const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
|
|
132
|
+
if(key!=='root')recordDeclaration(root,org,intent.id,{...declaration,name,parent:above?.id??null});
|
|
290
133
|
const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
|
|
291
134
|
lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
|
|
292
135
|
synced:!!intent.declared,main_session:null,revision:0};
|
|
@@ -357,38 +200,61 @@ function adoptLocalOrg(root,scope){
|
|
|
357
200
|
db.exec('COMMIT');
|
|
358
201
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
359
202
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
203
|
+
/** An explicit fork's child, declared under its parent and started by the fork's source session; the source must be a
|
|
204
|
+
* session this home ran for the parent. Local: the child's declaration is handed on like any other (handOffAgents). */
|
|
205
|
+
export function localForkAgent(options){
|
|
206
|
+
const {root,org,profiles,parent,session,name,key}=options;
|
|
207
|
+
const owner=agentRunForSession(root,[session]);
|
|
208
|
+
if(owner?.agent_id!==parent)throw new Error('fork source is not a session of the declared parent');
|
|
209
|
+
const parentIntent=agentIntent(root,org,parent);
|
|
210
|
+
if(!parentIntent)throw new Error('fork parent has no retained profile intent here');
|
|
211
|
+
let intent=declareAgentIntent({...options,profile:parentIntent.profile_ref,key:`fork/${parent}/${key}`,refresh:false});
|
|
212
|
+
if(endedHere(root,org,intent.id))throw new Error('this fork identity ended; declare a new fork key');
|
|
213
|
+
// The snapshot copies configuration only. Memory stays the new child's own empty state.
|
|
214
|
+
const intended=structuredClone(intent.fresh?parentIntent.profile:intent.intended);
|
|
215
|
+
const db=store(root);try{db.prepare('UPDATE agent_intents SET profile_json=? WHERE org=? AND id=?').run(JSON.stringify(intended),org,intent.id);}finally{db.close();}
|
|
216
|
+
recordDeclaration(root,org,intent.id,{kind:'fork',profile:parentIntent.profile_ref,name,parent,starterSession:session});
|
|
217
|
+
const assignment=assignAgent(root,org,intent.id,{kind:'fork',target:session,role:key});
|
|
218
|
+
const identity={id:intent.id,kind:'agent',org,name,path:name,parent,lineage:{starter:{kind:'session',id:session}},sponsor:null,explicit_sponsor:null,
|
|
219
|
+
profile:parentIntent.profile_ref,state:'active',synced:!!intent.declared,main_session:null,revision:0};
|
|
220
|
+
return {identity,intended,assignment,assignments:[{kind:'fork',target:session,role:key}],org,synced:false};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Hand this home's agent declarations and ends to this machine's daemon (`harness.v1.agents.declare|end`, its local
|
|
224
|
+
* socket), which declares and ends them in Teams (D137: identity is Teams' job, never the dispatcher's or the gateway's).
|
|
225
|
+
* Local only: a daemon not running keeps them here for the next hand-off. Answers what could not be handed on.
|
|
226
|
+
*/
|
|
227
|
+
export function handOffAgents(root,{context=process.env.SUPERCODE_CONTEXT}={}){
|
|
365
228
|
const scope=agentScope(context);if(scope.offline)return [];
|
|
366
229
|
adoptLocalOrg(root,scope);
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
230
|
+
const waiting=[];
|
|
231
|
+
let db=store(root),rows,ends;
|
|
232
|
+
try{
|
|
233
|
+
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);
|
|
234
|
+
ends=db.prepare('SELECT * FROM agent_end_outbox WHERE org=? ORDER BY created_at LIMIT 100').all(scope.org);
|
|
235
|
+
}finally{db.close();}
|
|
370
236
|
for(const row of rows){
|
|
371
237
|
const declaration=JSON.parse(row.declaration_json);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
238
|
+
const body={org:scope.org,id:row.id,name:declaration.name??declaration.profile,parent:declaration.parent??null,profile:declaration.profile,
|
|
239
|
+
...(declaration.kind==='root'?{root:true}:{}),...(declaration.sponsor?{sponsor:declaration.sponsor}:{}),
|
|
240
|
+
...(declaration.starterSession?{starter_session:declaration.starterSession}:{}),key:digest(row.key)};
|
|
241
|
+
const reply=runtimeDoor(null,'harness.v1.agents.declare',body,{timeout:15_000});
|
|
242
|
+
if(reply.code!==0){waiting.push({agent:row.id,error:`not handed to this machine's daemon: ${reply.stderr.trim().split('\n').at(-1)}`});break;}
|
|
243
|
+
db=store(root);try{db.prepare('UPDATE agent_intents SET declared=1 WHERE org=? AND id=?').run(scope.org,row.id);}finally{db.close();}
|
|
244
|
+
}
|
|
245
|
+
for(const row of ends){
|
|
246
|
+
db=store(root);let active;
|
|
247
|
+
try{active=db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(scope.org,row.agent_id);if(active)db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(scope.org,row.agent_id);}finally{db.close();}
|
|
248
|
+
if(active)continue;
|
|
249
|
+
const reply=runtimeDoor(null,'harness.v1.agents.end',{org:scope.org,agent:row.agent_id,reason:row.reason,key:digest(`${scope.org}/${row.agent_id}/end/${row.created_at}`)},{timeout:15_000});
|
|
250
|
+
if(reply.code!==0){waiting.push({agent:row.agent_id,error:`end not handed to this machine's daemon: ${reply.stderr.trim().split('\n').at(-1)}`});break;}
|
|
251
|
+
db=store(root);
|
|
376
252
|
try{
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
else if(declaration.kind==='declared')ensureProfileAgent({...base,profile:declaration.profile,agent:declaration.agent,declarations});
|
|
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
|
-
}
|
|
253
|
+
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,scope.org,row.agent_id);
|
|
254
|
+
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(scope.org,row.agent_id);
|
|
255
|
+
}finally{db.close();}
|
|
390
256
|
}
|
|
391
|
-
return
|
|
257
|
+
return waiting;
|
|
392
258
|
}
|
|
393
259
|
/** A managed runtime may report its native id after opening. Keep the lease pointer out of the catalogue. */
|
|
394
260
|
export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
@@ -399,42 +265,12 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
|
399
265
|
const db=store(root);try{
|
|
400
266
|
db.exec('BEGIN IMMEDIATE');
|
|
401
267
|
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);
|
|
402
|
-
//
|
|
403
|
-
db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=?
|
|
268
|
+
// The lease-only row is not a session: its native session is the run now.
|
|
269
|
+
db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=?').run(owner.org,runtimeRef);
|
|
404
270
|
db.exec('COMMIT');
|
|
405
271
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
406
272
|
return agentRunForSession(root,[runtimeRef]);
|
|
407
273
|
}
|
|
408
|
-
/** Join an observed native session through the public catalogue; unknown catalogue state remains pending. */
|
|
409
|
-
// A run whose session ownership Teams refused (a 4xx answer to the claim: not found, forbidden, conflict, signed out)
|
|
410
|
-
// is asked again after a growing wait (two minutes, doubling to an hour), not every round: each ask spends the shared
|
|
411
|
-
// credential's allowance. Anything else (the rate limit, a timeout, a 5xx, the local daemon's bind of a session just
|
|
412
|
-
// spawned) is not the run's own and is asked again next round.
|
|
413
|
-
// The wait is kept in this home's agent store, not in the asking process: a one-shot `workflow dispatch` asked again
|
|
414
|
-
// on every run when each process kept its own, and every ask spends the shared credential's allowance.
|
|
415
|
-
const retryOf=(root,org,row)=>{const db=store(root);try{return db.prepare('SELECT next_at,wait_ms FROM agent_run_retries WHERE org=? AND agent_id=? AND session_ref=?').get(org,row.agent_id,row.session_ref)??null;}finally{db.close();}};
|
|
416
|
-
const refused=error=>error?.status>=400&&error.status<500&&error.status!==408&&error.status!==429;
|
|
417
|
-
export function synchronizeAgentRun(root,org,row,{context,intended}={}) {
|
|
418
|
-
let answer;
|
|
419
|
-
try{answer=joinAgentRun(root,org,row,{context,intended});}
|
|
420
|
-
catch(error){
|
|
421
|
-
if(error?.claimRefused){
|
|
422
|
-
const wait=Math.min((retryOf(root,org,row)?.wait_ms??60000)*2,3600000),db=store(root);
|
|
423
|
-
try{db.prepare('INSERT INTO agent_run_retries(org,agent_id,session_ref,next_at,wait_ms) VALUES (?,?,?,?,?) ON CONFLICT(org,agent_id,session_ref) DO UPDATE SET next_at=excluded.next_at,wait_ms=excluded.wait_ms').run(org,row.agent_id,row.session_ref,Date.now()+wait,wait);}finally{db.close();}
|
|
424
|
-
}
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
const db=store(root);try{db.prepare('DELETE FROM agent_run_retries WHERE org=? AND agent_id=? AND session_ref=?').run(org,row.agent_id,row.session_ref);}finally{db.close();}
|
|
428
|
-
return answer;
|
|
429
|
-
}
|
|
430
|
-
function joinAgentRun(root,org,row,{context,intended}) {
|
|
431
|
-
const claim=(...args)=>{try{return agentDoor(...args);}catch(error){throw Object.assign(error,{claimRefused:refused(error)});}};
|
|
432
|
-
const response=claim('ownSession',[row.agent_id,{session:row.session_ref,main:row.main!==0&&row.main!==false,...(row.starter_session?{starter_session:row.starter_session}:{})},digest(`${org}/${row.agent_id}/${row.session_ref}`)],{context});
|
|
433
|
-
const intent=agentIntent(root,org,row.agent_id);
|
|
434
|
-
const runtime=agentDoor('bindContext',[row.agent_id,row.session_ref,row.machine,intent?.assignments??[],intended??intent?.profile.resolved],{context});
|
|
435
|
-
publishAgentRun(root,org,row.session_ref);
|
|
436
|
-
return {...response.data,runtime};
|
|
437
|
-
}
|
|
438
274
|
export function recordAgentRun(root,org,id,{session,machine=null,run,profile=null,main=true,starterSession=null}){
|
|
439
275
|
const db=store(root);try{
|
|
440
276
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -444,13 +280,6 @@ export function recordAgentRun(root,org,id,{session,machine=null,run,profile=nul
|
|
|
444
280
|
db.exec('COMMIT');
|
|
445
281
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
446
282
|
}
|
|
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();}}
|
|
454
283
|
/** Session references come from the bound runtime, never from a model's requested agent id. */
|
|
455
284
|
export function agentRunForSession(root, references) {
|
|
456
285
|
const db = store(root);
|
|
@@ -496,9 +325,6 @@ export function runBudgetRefusal(root,agent,budgets,now){
|
|
|
496
325
|
}
|
|
497
326
|
return null;
|
|
498
327
|
}
|
|
499
|
-
/** A run whose session Teams never had and that no longer runs here can never be owned there: it leaves the sync. */
|
|
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();}}
|
|
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();}}
|
|
502
328
|
export function agentPreamble(agent,{main=true,previous=[]}={}){
|
|
503
329
|
const {identity:a,intended,assignments=[]}=agent;
|
|
504
330
|
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'):''}`;
|
package/bin/agent-door.mjs
CHANGED
|
@@ -32,23 +32,6 @@ try{
|
|
|
32
32
|
result=agent.state==='ended'?{data:{id:agent.id,state:'ended'}}:await team.agents.end(agent.id,{reason:args[1]},{...options,ifMatch:agent.revision,idempotencyKey:args[2]});
|
|
33
33
|
}
|
|
34
34
|
else if(op==='reader')result=await team.agents.delegateReader(args[0],{},{...options,ifMatch:args[1]});
|
|
35
|
-
else if(op==='bindContext'){
|
|
36
|
-
// The binding names the agent's reader context on the session's machine, so it installs that context itself: a
|
|
37
|
-
// launch installs one before its session starts, but an adopted session never had one, and a context installed a
|
|
38
|
-
// day before has expired. An ended agent has neither a reader nor a live session: Teams revoked its credentials
|
|
39
|
-
// and ended its sessions when it ended, so there is nothing to bind.
|
|
40
|
-
const agent=(await team.agents.get(args[0],options)).data;
|
|
41
|
-
if(agent.state==='ended')result={bound:false,reason:`agent ${agent.id} ended`};
|
|
42
|
-
else{
|
|
43
|
-
const reader=await team.agents.delegateReader(agent.id,{expires_in_ms:86400000},{...options,ifMatch:agent.revision});
|
|
44
|
-
const channel=await openMachine({machine:reach(args[2]),context:context??process.env.SUPERCODE_CONTEXT});
|
|
45
|
-
try{
|
|
46
|
-
const rpc=new StreamRpc(await channel.open('node'));
|
|
47
|
-
const installed=await rpc.call('harness.v1.agents.context',{origin:scope.origin,team:scope.team_id,agent:agent.id,credential:reader.data});
|
|
48
|
-
result=await rpc.call('harness.v1.agents.bindSession',{session:args[1],context:installed.context,assignments:args[3]??[],intended:args[4]??null});
|
|
49
|
-
}finally{channel.close();}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
35
|
else if(op==='readerContext'){
|
|
53
36
|
const identity=await team.agents.get(args[0],options);
|
|
54
37
|
const reader=await team.agents.delegateReader(args[0],{expires_in_ms:86400000},{...options,ifMatch:identity.data.revision});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// Consume the card-creation intent after the board transaction; retries reuse the daemon-minted id.
|
|
2
|
-
|
|
1
|
+
// Consume the card-creation intent after the board transaction; retries reuse the daemon-minted id. The card's agent is
|
|
2
|
+
// minted and recorded here, and its declaration handed to this machine's daemon, which declares it in Teams: whoever ran
|
|
3
|
+
// the effect (the creating command, or a delivery pass retrying it) makes no Teams call (D137).
|
|
4
|
+
import { agentScope, localCardAgent, finishAgentAssignment, activeAgentAssignments, handOffAgents } from '../agents.mjs';
|
|
3
5
|
import { event, json, tx, view } from './store.mjs';
|
|
4
6
|
import { workflowFor } from './workflow.mjs';
|
|
5
7
|
|
|
@@ -15,15 +17,26 @@ export function declareCardAgentEffect(board,effect) {
|
|
|
15
17
|
const context=effect.context??process.env.SUPERCODE_CONTEXT,scope=agentScope(context);
|
|
16
18
|
const options={root:board.root,org:scope.org,server:scope.server,profiles:entry.profiles,
|
|
17
19
|
profile,card:effect.id,role:effect.role,sponsor:task.sponsor??null,context,starterSession:effect.starter_session};
|
|
18
|
-
|
|
19
|
-
let agent;
|
|
20
|
-
if(scope.offline)agent=localCardAgent(options);
|
|
21
|
-
else try{agent=ensureCardAgent(options);}catch(error){if(!teamsUnavailable(error))throw error;agent=localCardAgent(options);}
|
|
20
|
+
const agent=localCardAgent(options);
|
|
22
21
|
if(task.archived||['done','cancelled','archived'].includes(task.status))finishAgentAssignment(board.root,scope.org,agent.assignment,`card ${effect.id} ${task.status}`);
|
|
23
|
-
|
|
22
|
+
// A daemon not running keeps the declaration here; every delivery pass hands it on again.
|
|
23
|
+
const waiting=handOffAgents(board.root,{context});
|
|
24
|
+
tx(board,db=>event(db,effect.id,'agent_declared',{agent:agent.identity.id,org:scope.org,role:effect.role,profile,assignment:agent.assignment,handed_to_machine:!waiting.length}));
|
|
24
25
|
return true;
|
|
25
26
|
}catch(error) {
|
|
26
27
|
if(!told.has(error.message))tx(board,db=>event(db,effect.id,'agent_declaration_pending',{role:effect.role,profile,reason:error.message}));
|
|
27
28
|
return false;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
/** The card agents of `ids` whose cards are done, cancelled or archived end their assignments, and an agent left with
|
|
33
|
+
* none has its end handed to this machine's daemon: run by the verb that closed the card, so no dispatcher is needed. */
|
|
34
|
+
export function endFinishedAgents(board,ids){
|
|
35
|
+
const wanted=new Set(ids.map(String)),assignments=activeAgentAssignments(board.root).filter(item=>item.kind==='card'&&wanted.has(String(item.target)));
|
|
36
|
+
if(!assignments.length)return [];
|
|
37
|
+
for(const assignment of assignments){
|
|
38
|
+
const task=view(board,db=>db.prepare('SELECT t.status,c.archived FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(assignment.target));
|
|
39
|
+
if(task&&(task.archived||['done','cancelled','archived'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
40
|
+
}
|
|
41
|
+
return handOffAgents(board.root);
|
|
42
|
+
}
|
package/board/dispatch.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { readMachineFleet } from './machine-fleet.mjs';
|
|
|
20
20
|
let fleetRoot = 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 { agentScope, localCardAgent, localReaderContext, localAgentSessions,
|
|
23
|
+
import { agentScope, localCardAgent, localReaderContext, localAgentSessions, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, activeAgentAssignments,finishAgentAssignment,handOffAgents, 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';
|
|
@@ -160,11 +160,6 @@ const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ?
|
|
|
160
160
|
const sessionAt = (address) => { const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(address)); return m && HARNESSES[m[2]] ? { address, machine: normalMachine(m[1]), harness: m[2], session_id: m[3] } : null; };
|
|
161
161
|
const subscriberAlive = (fleet, address) => fleet.alive.has(address) || (sessionAt(address) ? sessionAlive(fleet, sessionAt(address)) === true : false);
|
|
162
162
|
const paneLists = new Map();
|
|
163
|
-
// A run the agent sync holds names only its machine and native session (`harness:id`), never a pane or pid.
|
|
164
|
-
function syncRowAlive(fleet, row) {
|
|
165
|
-
const machine = row.machine || localMachine(), at = row.session_ref.indexOf(':');
|
|
166
|
-
return sessionAlive(fleet, { machine, harness: row.session_ref.slice(0, at), session_id: row.session_ref.slice(at + 1), address: `sc:${machine}:${row.session_ref}` });
|
|
167
|
-
}
|
|
168
163
|
function unpublishedAlive(session) {
|
|
169
164
|
if (!session) return null;
|
|
170
165
|
if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
|
|
@@ -1307,25 +1302,25 @@ export function reconcileCallerLaunch(board, sid) {
|
|
|
1307
1302
|
}
|
|
1308
1303
|
}
|
|
1309
1304
|
|
|
1310
|
-
/** The board's agent records
|
|
1311
|
-
*
|
|
1312
|
-
|
|
1305
|
+
/** The board's agent records: each run kept in the agent store, closed cards' assignments ended, declarations and ends
|
|
1306
|
+
* handed to this machine's daemon, and usage read from each machine's own doors. No Teams call: the daemon declares and
|
|
1307
|
+
* ends agents and binds their sessions (D137). It runs in the delivery pass (`deliver`), never in a dispatch round. */
|
|
1308
|
+
function synchronizeAgents(board, report) {
|
|
1313
1309
|
// A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
|
|
1314
1310
|
// A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
|
|
1315
1311
|
const agentRuns=[...new Map(view(board,db=>db.prepare("SELECT id,task_id,metadata,ended_at FROM task_runs WHERE json_extract(metadata,'$.supercode.agent_id') IS NOT NULL ORDER BY id").all())
|
|
1316
1312
|
.map(row=>{const session=json(row.metadata,{})?.supercode;return [session?.session_id?addressOf(session):`run:${row.id}`,row];})).values()];
|
|
1317
|
-
const orgs=new Map();
|
|
1318
1313
|
for(const row of agentRuns){
|
|
1319
1314
|
const session=json(row.metadata,{})?.supercode;
|
|
1320
1315
|
if(!session?.session_id||!session.agent_org)continue;
|
|
1321
1316
|
const ref=`${session.harness}:${session.session_id}`;
|
|
1322
|
-
// An ended run whose session the agent store already holds
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1317
|
+
// An ended run whose session the agent store already holds for another agent lost that claim when it ran: no round
|
|
1318
|
+
// can change an ended run, so the disagreement is history. It is said once on its card (agent_ownership_conflict)
|
|
1319
|
+
// and the run is not recorded again. A live run is never history: it is recorded below every round, and a conflict
|
|
1320
|
+
// there is refused and said each round as before.
|
|
1326
1321
|
if(row.ended_at){
|
|
1327
1322
|
let owner=null;try{owner=agentRunForSession(board.root,[ref]);}catch{/* conflicting owners: recorded below, refused there */}
|
|
1328
|
-
if(owner
|
|
1323
|
+
if(owner&&owner.org===session.agent_org&&owner.agent_id!==session.agent_id){
|
|
1329
1324
|
const said=db=>db.prepare("SELECT 1 FROM task_events WHERE task_id=? AND run_id=? AND kind='agent_ownership_conflict' LIMIT 1").get(row.task_id,row.id);
|
|
1330
1325
|
if(!view(board,said))tx(board,db=>{if(!said(db))event(db,row.task_id,'agent_ownership_conflict',{session:ref,run_agent:session.agent_id,owner:owner.agent_id,owner_run:owner.run_id},row.id);});
|
|
1331
1326
|
continue;
|
|
@@ -1334,16 +1329,7 @@ function synchronizeAgents(board, fleet, report, entry) {
|
|
|
1334
1329
|
// One run whose session another agent already owns is that card's problem, said on it; the round goes on for the rest.
|
|
1335
1330
|
try{recordAgentRun(board.root,session.agent_org,session.agent_id,{session:ref,machine:session.machine,run:row.id});}
|
|
1336
1331
|
catch(error){report.queued.push([row.task_id,`run ${row.id}: ${error.message}`]);continue;}
|
|
1337
|
-
orgs.set(session.agent_org,session.agent_context);
|
|
1338
|
-
}
|
|
1339
|
-
// Identities minted while Teams was absent are declared under their ids before their sessions are joined.
|
|
1340
|
-
const context=process.env.SUPERCODE_CONTEXT;
|
|
1341
|
-
let scope;try{scope=agentScope(context);}catch(error){scope={offline:true};report.failed.push(['agents',`agent declarations wait: ${error.message}`]);}
|
|
1342
|
-
if(!scope.offline){
|
|
1343
|
-
orgs.set(scope.org,orgs.get(scope.org)??context);
|
|
1344
|
-
for(const pending of synchronizeLocalAgents({root:board.root,profiles:entry.profiles,context}))(pending.failed?report.failed:report.queued).push([pending.agent,`agent declaration ${pending.failed?`failed for good after ${pending.attempts} refusals`:'pending'}: ${pending.error}`]);
|
|
1345
1332
|
}
|
|
1346
|
-
orgs.delete('local');
|
|
1347
1333
|
// G3: an ended run's session usage, read once from its harness's own records on its machine
|
|
1348
1334
|
for(const org of new Set(agentRuns.map(row=>json(row.metadata,{})?.supercode?.agent_org).filter(Boolean))){
|
|
1349
1335
|
let read=0;
|
|
@@ -1365,14 +1351,9 @@ function synchronizeAgents(board, fleet, report, entry) {
|
|
|
1365
1351
|
const task=getTaskNow(board,assignment.target);
|
|
1366
1352
|
if(task&&(task.archived||['done','cancelled'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
1367
1353
|
}
|
|
1368
|
-
|
|
1369
|
-
for(const
|
|
1370
|
-
|
|
1371
|
-
// each run is claimed by compare-and-set on its own `published` before it is joined; another pass's claim skips it
|
|
1372
|
-
if(!claimAgentRun(board.root,org,row))continue;
|
|
1373
|
-
try{synchronizeAgentRun(board.root,org,row,{context,intended:json(launch?.metadata,{})?.supercode?.agent_profile_intent});}
|
|
1374
|
-
catch(error){releaseAgentRun(board.root,org,row);if(error.code==='rate_limited'){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);break;}const gone=error.claimRefused&&error.status===404&&fleet.ok&&syncRowAlive(fleet,row)===false;if(gone)abandonAgentRun(board.root,org,row.session_ref);report.failed.push([row.run_id,`agent session sync failed: ${error.message}${gone?`; ${row.session_ref} is stopped and Teams refuses it, so it leaves the sync`:''}`]);}
|
|
1375
|
-
}
|
|
1354
|
+
// What a creating or closing verb could not hand on (no daemon running then) is handed on here.
|
|
1355
|
+
try{for(const pending of handOffAgents(board.root))report.queued.push([pending.agent,pending.error]);}
|
|
1356
|
+
catch(error){report.queued.push(['agents',`agent hand-off waits: ${error.message}`]);}
|
|
1376
1357
|
}
|
|
1377
1358
|
|
|
1378
1359
|
function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
|
|
@@ -2668,7 +2649,7 @@ export async function deliver({ root, slugs = null }) {
|
|
|
2668
2649
|
tx(board, (db) => queueBoardMail(db)); phase('mail');
|
|
2669
2650
|
report.effects = await runEffects(board, [], { pending: true }); phase('effects');
|
|
2670
2651
|
await notify(board, fleet, report, entry); phase('notify');
|
|
2671
|
-
synchronizeAgents(board,
|
|
2652
|
+
synchronizeAgents(board, report); phase('agents');
|
|
2672
2653
|
report.effects_failed = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NOT NULL').get().n);
|
|
2673
2654
|
report.effects_waiting = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NULL').get().n);
|
|
2674
2655
|
} catch (error) { report.error = error.message; }
|
package/board/engine.mjs
CHANGED
|
@@ -418,6 +418,8 @@ export async function act(board, id, name, { roles = null, payload = {}, extra =
|
|
|
418
418
|
return r;
|
|
419
419
|
});
|
|
420
420
|
await runEffects(board, result.effects);
|
|
421
|
+
// A card this verb closed ends its agents' assignments; the verb hands the ends on itself (agent-declarations.mjs).
|
|
422
|
+
if (result.moved) { const { endFinishedAgents } = await import('./agent-declarations.mjs'); endFinishedAgents(board, [id]); }
|
|
421
423
|
return { from: result.from, to: result.to, moved: result.moved, ...(result.commentId ? { commentId: result.commentId } : {}) };
|
|
422
424
|
}
|
|
423
425
|
|
|
@@ -445,6 +447,7 @@ export async function requestStateMany(board, ids, payload = {}) {
|
|
|
445
447
|
return { results, effects };
|
|
446
448
|
});
|
|
447
449
|
await runEffects(board, result.effects);
|
|
450
|
+
{ const { endFinishedAgents } = await import('./agent-declarations.mjs'); endFinishedAgents(board, targets); }
|
|
448
451
|
return result.results;
|
|
449
452
|
}
|
|
450
453
|
|
package/index.d.ts
CHANGED
|
@@ -110,33 +110,28 @@ export function agentStateHome(root: Path, org: string, id: string): Path;
|
|
|
110
110
|
export function recordNativeAgentSession(root: Path, runtimeRef: string, session: string): Record<string, unknown> | null;
|
|
111
111
|
export function materializeAgentHome(root: Path, org: string, id: string, profile: Profile): Path;
|
|
112
112
|
export function declareAgentIntent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; key: string; machine?: string | null}): Record<string, unknown>;
|
|
113
|
-
export function ensureRootAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile?: string; context?: string; machine?: string | null}): AgentIdentity;
|
|
114
|
-
export function ensureCardAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; card: string; role: string; sponsor?: string | null; context?: string; machine?: string | null; starterSession?: string | null}): {identity: AgentIdentity; intended: ReturnType<typeof resolveAgentProfile>; assignment: string; assignments: Array<Record<string, unknown>>; org: string};
|
|
115
|
-
export function ensureProfileAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; context?: string; machine?: string | null}): ReturnType<typeof ensureCardAgent>;
|
|
116
|
-
export function ensureForkAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; parent: string; session: string; name: string; key: string; context?: string; machine?: string | null}): ReturnType<typeof ensureCardAgent>;
|
|
117
113
|
export function agentRunForKey(root: Path, org: string, agent: string, key: string): Record<string, unknown> | null;
|
|
118
114
|
export function agentIntent(root: Path, org: string, id: string): Record<string, unknown> | null;
|
|
119
115
|
export function assignAgent(root: Path, org: string, id: string, assignment: {kind: string; target: string; role: string}): string;
|
|
120
116
|
export function reassignAgent(root: Path, org: string, assignment: string, to: string, options?: {context?: string}): Record<string, unknown>;
|
|
121
117
|
export function activeAgentAssignments(root: Path): Array<Record<string, unknown>>;
|
|
122
118
|
export function finishAgentAssignment(root: Path, org: string, assignment: string, reason: string): void;
|
|
123
|
-
export function synchronizeAgentEnds(root: Path, org: string, options?: {context?: string}): Array<{agent: string; error: string}>;
|
|
124
119
|
export function recordAgentRun(root: Path, org: string, id: string, run: {session: string; machine?: string | null; run: string | number; profile?: unknown; main?: boolean; starterSession?: string | null}): void;
|
|
125
|
-
export function pendingAgentRuns(root: Path, org: string): Array<Record<string, unknown>>;
|
|
126
|
-
export function abandonAgentRun(root: Path, org: string, session: string): void;
|
|
127
|
-
export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: string; session_ref: string; machine?: string | null; main?: boolean | number; starter_session?: string | null}, options?: {context?: string; intended?: unknown}): import('@volter/teams/http-client').SessionIdentity & {runtime: unknown};
|
|
128
120
|
export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
|
|
129
|
-
export function agentPreamble(agent:
|
|
130
|
-
export function agentEnvironment(agent:
|
|
121
|
+
export function agentPreamble(agent: LocalAgent, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
|
|
122
|
+
export function agentEnvironment(agent: LocalAgent, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
|
|
131
123
|
/** 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
124
|
export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
|
|
133
|
-
export function teamsUnavailable(error: unknown): boolean;
|
|
134
125
|
/** 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
|
-
|
|
136
|
-
export
|
|
137
|
-
export function
|
|
126
|
+
/** A local agent: its daemon-minted id, retained intent and assignments. */
|
|
127
|
+
export type LocalAgent = {identity: AgentIdentity; intended: ReturnType<typeof resolveAgentProfile>; assignment: string; assignments: Array<Record<string, unknown>>; org: string};
|
|
128
|
+
export function localCardAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; card: string; role: string; sponsor?: string | null; machine?: string | null; starterSession?: string | null}): LocalAgent & {synced: false};
|
|
129
|
+
export function localProfileAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; machine?: string | null; agent?: string | null; declarations?: Record<string, unknown>}): LocalAgent & {synced: false};
|
|
130
|
+
export function localForkAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; parent: string; session: string; name: string; key: string}): LocalAgent;
|
|
131
|
+
/** Hand this home's agent declarations and ends to this machine's daemon, which declares and ends them in Teams. Answers what could not be handed on. */
|
|
132
|
+
export function handOffAgents(root: Path, options?: {context?: string}): Array<{agent: string; error: string}>;
|
|
133
|
+
export function localAgentRecord(root: Path, org: string, id: string): LocalAgent | null;
|
|
138
134
|
export function localAgentSessions(root: Path, org: string, id: string): Array<{session: string; machine: string | null; ended_at: number | null; end_reason: string | null}>;
|
|
139
|
-
export function synchronizeLocalAgents(options: {root: Path; profiles: Record<string, Profile>; context?: string; machine?: string | null}): Array<{agent: string; error: string}>;
|
|
140
135
|
export function agentProfileMismatch(intent: unknown, resolved: unknown): import('@volter/teams/agent-profile').ProfileComparison;
|
|
141
136
|
|
|
142
137
|
export type ChatType = 'dm' | 'group' | 'channel' | 'thread';
|
package/index.mjs
CHANGED
|
@@ -39,5 +39,5 @@ export { hermesAccess, recordIncident, hermesToolEnv, hermesInstall } from './he
|
|
|
39
39
|
export { holdGatewayLock, gatewayLockHeld, acquireTokenLock, releaseTokenLock, looksLikeGateway, TOKEN_SCOPES } from './hermes-locks.mjs';
|
|
40
40
|
|
|
41
41
|
export { resolveAgentProfile, profileDifferences, sessionAgent } from "./agent-profiles.mjs";
|
|
42
|
-
export { agentScope,
|
|
42
|
+
export { agentScope, localCardAgent, localProfileAgent, localAgentRecord, localAgentSessions, declareAgentIntent, agentRunForKey, agentIntent, assignAgent, reassignAgent, activeAgentAssignments, finishAgentAssignment, recordAgentRun, recordNativeAgentSession, agentRunForSession, agentPreamble, agentEnvironment, agentProfileMismatch, localForkAgent, handOffAgents} from "./agents.mjs";
|
|
43
43
|
export { agentStateHome,materializeAgentHome } from './agent-home.mjs';
|
package/kanban-worker.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
12
12
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
14
14
|
import { load } from './orchestration.mjs';
|
|
15
|
-
import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
15
|
+
import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, declareWorkerHome, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
16
16
|
import { workerRoute } from './worker-route.mjs';
|
|
17
17
|
import { KANBAN_ENV, untrustedServerVerdict, hermesApproval, hermesInstall, memorySnapshot } from './hermes-cli.mjs';
|
|
18
18
|
import { pinnedSkills, taskProtocol } from './board/worker-prompt.mjs';
|
|
@@ -94,6 +94,7 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
|
|
|
94
94
|
arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...sandboxArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd }),
|
|
95
95
|
env: { ...(base.env ?? {}), ...runEnv },
|
|
96
96
|
};
|
|
97
|
+
declareWorkerHome({ harness, env: launch.env, cwd, log: (_level, line) => say(`hermes-worker: ${line}`) });
|
|
97
98
|
const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
|
|
98
99
|
const policy = approvalPolicy(profile, harness);
|
|
99
100
|
const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
|
package/loop.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { basename, join, dirname } from 'node:path';
|
|
|
9
9
|
import { step, leaseId } from './reducer.mjs';
|
|
10
10
|
import * as events from './events.mjs';
|
|
11
11
|
import { read, save, retainWorld, releaseWorld } from './orchestration.mjs';
|
|
12
|
-
import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, approvalPolicy, sandboxArguments, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
12
|
+
import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, declareWorkerHome, approvalPolicy, sandboxArguments, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
13
13
|
import { workerRoute } from './worker-route.mjs';
|
|
14
14
|
import { hermesAccess, hermesApproval, untrustedServerVerdict, hermesToolEnv, memorySnapshot, mirrorSession, mirrorState, recordIncident, sealMirror } from './hermes-cli.mjs';
|
|
15
15
|
import { hermesSessionKey, homeChannel } from './ir.mjs';
|
|
@@ -24,7 +24,7 @@ import { harnessHomes } from './mcp/verbs.mjs';
|
|
|
24
24
|
import { checkMonitor, runJobScript } from './gates.mjs';
|
|
25
25
|
import { startBoard } from './board.mjs';
|
|
26
26
|
import { SenderIdentityRecord } from '@volter/teams/sender-identity';
|
|
27
|
-
import {
|
|
27
|
+
import { agentScope,localProfileAgent,localAgentRecord,localAgentSessions,localForkAgent,handOffAgents,localReaderContext,agentRunForKey,agentPreamble,agentEnvironment,recordAgentRun,recordNativeAgentSession,agentRunForSession } from './agents.mjs';
|
|
28
28
|
import { materializeAgentHome } from './agent-home.mjs';
|
|
29
29
|
import { localMachine } from './board/dispatch.mjs';
|
|
30
30
|
import { execFile } from 'node:child_process';
|
|
@@ -189,23 +189,17 @@ export class Orchestrator {
|
|
|
189
189
|
tick(at = this.clock()) {
|
|
190
190
|
this.queue = this.queue.then(() => this.adoptOutsideJobs()).catch((error) => this.log('warn', `cron/jobs.json not read back: ${error.message}`));
|
|
191
191
|
return this.dispatch(events.tick(at, cronInactivitySeconds(), cronMaxParallel())).then(
|
|
192
|
-
(result) => { this.tickerHeartbeat(true); this.
|
|
192
|
+
(result) => { this.tickerHeartbeat(true); this.declareLocalAgents();return result; },
|
|
193
193
|
(error) => { this.tickerHeartbeat(false); throw error; },
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
/** The home's agent declarations and ends a launch could not hand to this machine's daemon (none was running) are
|
|
198
|
+
* handed on. The daemon declares and ends them in Teams and binds their sessions; the gateway makes no Teams call. */
|
|
199
|
+
declareLocalAgents(){
|
|
198
200
|
if(!existsSync(join(this.state.root,'agent-state.sqlite')))return;
|
|
199
|
-
try{
|
|
200
|
-
|
|
201
|
-
if(scope.offline)return;
|
|
202
|
-
for(const pending of synchronizeLocalAgents({root:this.state.root,profiles:this.state.profiles,context,declarations:this.state.agents??{}}))this.log('warn',`agent ${pending.agent} declaration pending: ${pending.error}`);
|
|
203
|
-
for(const row of pendingAgentRuns(this.state.root,scope.org)){
|
|
204
|
-
if(!claimAgentRun(this.state.root,scope.org,row))continue;
|
|
205
|
-
try{synchronizeAgentRun(this.state.root,scope.org,row,{context,intended:row.profile_json?JSON.parse(row.profile_json):undefined});}
|
|
206
|
-
catch(error){releaseAgentRun(this.state.root,scope.org,row);this.log('warn',`agent session ${row.session_ref}: ${error.message}`);if(error.code==='rate_limited')break;}
|
|
207
|
-
}
|
|
208
|
-
}catch(error){this.log('warn',`agent session synchronization: ${error.message}`);}
|
|
201
|
+
try{for(const pending of handOffAgents(this.state.root))this.log('warn',`agent ${pending.agent}: ${pending.error}`);}
|
|
202
|
+
catch(error){this.log('warn',`agent hand-off: ${error.message}`);}
|
|
209
203
|
}
|
|
210
204
|
|
|
211
205
|
/**
|
|
@@ -422,42 +416,21 @@ export class Orchestrator {
|
|
|
422
416
|
materializeSkills(profile);
|
|
423
417
|
let launch,agent,scope,context,agentPrompt,main,agentHome;
|
|
424
418
|
try {
|
|
419
|
+
// The launch asks Teams nothing (D137): the agent is this home's minted identity, its declaration handed to this
|
|
420
|
+
// machine's daemon, its reader context the one this machine holds (the daemon delegates one when it binds the
|
|
421
|
+
// session), its earlier sessions the ones this home ran.
|
|
425
422
|
context=process.env.SUPERCODE_CONTEXT;scope=agentScope(context);
|
|
426
|
-
|
|
427
|
-
let previous=null,reader=null;
|
|
423
|
+
let previous=null;
|
|
428
424
|
if(agent_as) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if(!intent)throw new Error('launch as an agent requires its retained profile intent');
|
|
433
|
-
agent={identity,intended:intent.profile,assignments:intent.assignments,org:scope.org};
|
|
434
|
-
} else if(!scope.offline) {
|
|
435
|
-
try{agent=ensureProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,context,agent:agentName,declarations:this.state.agents});}catch(error){local(error);}
|
|
425
|
+
const record=localAgentRecord(this.state.root,scope.org,agent_as);
|
|
426
|
+
if(!record)throw new Error('launch as an agent requires its retained profile intent');
|
|
427
|
+
agent=record;
|
|
436
428
|
}
|
|
437
|
-
// Situation 16: no reachable Teams. The locally minted identity runs now and is declared later under its id.
|
|
438
429
|
agent??=localProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,agent:agentName,declarations:this.state.agents});
|
|
430
|
+
if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
439
431
|
agent.intended.resolved.worker={...agent.intended.resolved.worker,...worker,cwd};
|
|
440
432
|
main=new_fork||!agent.identity.main_session;
|
|
441
|
-
if(
|
|
442
|
-
try{
|
|
443
|
-
previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
|
|
444
|
-
if(resume_runtime_id&&!new_fork) {
|
|
445
|
-
// a session no sync rule publishes is unknown to Teams: its owner is the local run record, read below
|
|
446
|
-
let resumed=null;
|
|
447
|
-
try{resumed=agentDoor('session',[`${worker.harness}:${resume_runtime_id}`],{context}).data;}catch(error){if(error.code!=='not_found')throw error;}
|
|
448
|
-
if(resumed){
|
|
449
|
-
if(resumed.agent?.id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
|
|
450
|
-
main=resumed.main;
|
|
451
|
-
} else {
|
|
452
|
-
const owner=agentRunForSession(this.state.root,[`${worker.harness}:${resume_runtime_id}`]);
|
|
453
|
-
if(owner&&owner.agent_id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
|
|
454
|
-
if(owner)main=owner.main===1;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
reader=agentDoor('readerContext',[agent.identity.id,null],{context,timeout:60000});
|
|
458
|
-
}catch(error){local(error);}
|
|
459
|
-
}
|
|
460
|
-
if(resume_runtime_id&&!new_fork&&!previous){
|
|
433
|
+
if(resume_runtime_id&&!new_fork){
|
|
461
434
|
const owner=agentRunForSession(this.state.root,[`${worker.harness}:${resume_runtime_id}`]);
|
|
462
435
|
if(owner&&owner.agent_id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
|
|
463
436
|
if(owner)main=owner.main===1;
|
|
@@ -472,8 +445,9 @@ export class Orchestrator {
|
|
|
472
445
|
const route = workerRoute(profile, worker, this.vault);
|
|
473
446
|
// a worker of any harness but Hermes gets Hermes's CLI against its profile's home (row 7)
|
|
474
447
|
const tools = worker.harness === 'hermes' ? {} : hermesToolEnv(agentHome);
|
|
475
|
-
const env = { ...tools, ...activationEnv({ root: this.state.root, profile, surfaceKey: keyString, vault: this.vault }), ...route.env,...agentEnvironment(agent,{root:this.state.root,context:
|
|
448
|
+
const env = { ...tools, ...activationEnv({ root: this.state.root, profile, surfaceKey: keyString, vault: this.vault }), ...route.env,...agentEnvironment(agent,{root:this.state.root,context:localReaderContext(scope.org,agent.identity.id)??undefined,main}) };
|
|
476
449
|
env.HERMES_HOME=agentHome;
|
|
450
|
+
declareWorkerHome({ harness: worker.harness, env, cwd, log: (level, message) => this.log(level, message) });
|
|
477
451
|
// All sessions of this agent carry its state; a shared profile contributes configuration only.
|
|
478
452
|
const memory = INSTRUCTION_ARGUMENTS[worker.harness] ? await memorySnapshot(agentHome).catch((error) => { this.log('warn', `memory ${keyString}: ${error.message}`); return ''; }) : '';
|
|
479
453
|
// a worker in the user's own home carries its persona there too (its config home's CLAUDE.md is the user's)
|
|
@@ -522,8 +496,6 @@ export class Orchestrator {
|
|
|
522
496
|
if(!INSTRUCTION_ARGUMENTS[worker.harness])(this.runtimeAgentPreambles??=new Map()).set(lid,agentPrompt);
|
|
523
497
|
const session=`${worker.harness}:${runtime.handle.runtime_id}`;
|
|
524
498
|
recordAgentRun(this.state.root,scope.org,agent.identity.id,{session,machine:null,run:run_key??lid,profile:agent.intended.resolved,main,starterSession:starter_session});
|
|
525
|
-
if(agent.synced!==false&&claimAgentRun(this.state.root,scope.org,{session_ref:session,published:0}))try{synchronizeAgentRun(this.state.root,scope.org,{agent_id:agent.identity.id,session_ref:session,machine:null,main},{context,intended:agent.intended.resolved});}
|
|
526
|
-
catch(error){releaseAgentRun(this.state.root,scope.org,{session_ref:session});this.log('warn',`agent session ${session} retained for synchronization: ${error.message}`);}
|
|
527
499
|
this.enqueue(events.worker({
|
|
528
500
|
profile: profileName, keyString, runtime_id: runtime.handle.runtime_id,
|
|
529
501
|
ev: { kind: 'started', runtime_id: runtime.handle.runtime_id, session_id: runtime.handle.runtime_id, connection: runtime.connection, harness: worker.harness, steer },
|
|
@@ -542,8 +514,9 @@ export class Orchestrator {
|
|
|
542
514
|
if(!binding||binding.ended_at)throw new Error('register the fork target conversation before starting its runtime');
|
|
543
515
|
if(this.runtimes.has(leaseId(profileName,keyString)))throw new Error('fork requires an unoccupied target conversation');
|
|
544
516
|
if(!(await this.descriptorFor(locator.harness))?.runtime?.capabilities?.resume_session)throw new Error('this harness cannot resume a materialized fork');
|
|
545
|
-
const context=process.env.SUPERCODE_CONTEXT,scope=
|
|
546
|
-
const child=
|
|
517
|
+
const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context),session=`${locator.harness}:${locator.session_id}`;
|
|
518
|
+
const child=localForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key});
|
|
519
|
+
if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
|
|
547
520
|
const runKey=`fork:${key}`,prior=agentRunForKey(this.state.root,scope.org,child.identity.id,runKey);
|
|
548
521
|
let target;
|
|
549
522
|
if(prior) {
|
|
@@ -585,19 +558,10 @@ export class Orchestrator {
|
|
|
585
558
|
let preamble=null;
|
|
586
559
|
if(owner) {
|
|
587
560
|
try {
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
if(!
|
|
591
|
-
|
|
592
|
-
const live=agentDoor('session',[owner.session_ref],{context}).data;
|
|
593
|
-
if(live.agent?.id!==owner.agent_id||live.agent.state!=='active')throw new Error('retained session agent is no longer active or has different ownership');
|
|
594
|
-
const previous=agentDoor('sessions',[owner.agent_id,{limit:200}],{context}).items??[];
|
|
595
|
-
preamble=agentPreamble({identity:live.agent,intended:intent.profile,assignments:intent.assignments},{main:live.main,previous});
|
|
596
|
-
}catch(error){
|
|
597
|
-
// Teams cannot be asked: the session continues as the agent this home recorded, never as another.
|
|
598
|
-
if(!teamsUnavailable(error))throw error;
|
|
599
|
-
preamble=agentPreamble(localAgentRecord(this.state.root,owner.org,owner.agent_id),{main:owner.main===1,previous:localAgentSessions(this.state.root,owner.org,owner.agent_id)});
|
|
600
|
-
}
|
|
561
|
+
// The session continues as the agent this home recorded for it, never as another; Teams is not asked.
|
|
562
|
+
const record=localAgentRecord(this.state.root,owner.org,owner.agent_id);
|
|
563
|
+
if(!record)throw new Error('retained session has no agent profile intent');
|
|
564
|
+
preamble=agentPreamble(record,{main:owner.main===1,previous:localAgentSessions(this.state.root,owner.org,owner.agent_id)});
|
|
601
565
|
}catch(error){return interrupted(`agent identity unavailable: ${error.message}`);}
|
|
602
566
|
}
|
|
603
567
|
let capable = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.44",
|
|
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.16",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|