@volter/supercode-orchestrator 0.5.43 → 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/agents.mjs CHANGED
@@ -13,18 +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
- // The door's answer to a failure is its last stderr line; a runtime's own lines may come before it (a preload's notice).
19
- if(reply.status!==0){let detail;try{detail=JSON.parse(String(reply.stderr).trim().split('\n').at(-1));}catch{detail={message:reply.error?.message??String(reply.stderr).slice(-1000)};}throw Object.assign(new Error(detail.message??'Teams agent door unavailable'),detail);}
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);}
20
22
  return JSON.parse(reply.stdout);
21
23
  }
22
- /** Teams is not configured, not reachable, or no longer accepts this login: identity stays local until it syncs.
23
- * A refusal Teams decided (forbidden, conflict, not found, invalid) is never read as absence. */
24
- export function teamsUnavailable(error){
25
- if(error?.status!=null)return error.status>=500||error.status===401;
26
- return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
27
- }
28
24
  /** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
29
25
  * or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
30
26
  export function agentScope(context){return agentDoor('scope',[],{context});}
@@ -38,11 +34,6 @@ function store(root){
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
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));`);
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');
46
37
  return db;
47
38
  }
48
39
  const digest=value=>createHash('sha256').update(value).digest('hex');
@@ -86,24 +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
- // claimed by compare-and-set on its claimed_at (free, or a claim older than SYNC_CLAIM_MS from a pass that died);
95
- // deleted only after Teams ended the agent (the end is idempotent by its digest), handed back when it failed
96
- 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;
97
- try{
98
- agentDoor('end',[row.agent_id,row.reason,digest(`${org}/${row.agent_id}/end/${row.created_at}`)],{context});
99
- 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);
100
- db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,row.agent_id);
101
- }
102
- 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});}
103
- }
104
- return pending;
105
- }finally{db.close();}
106
- }
107
80
  /** Mint and retain intent before any network call. Offline declarations keep their immutable id for sync. */
108
81
  export function declareAgentIntent({root,org,server,profiles,profile,key,machine=null,refresh=true}) {
109
82
  const intended=resolveAgentProfile(profiles,profile),stable=`${server}/${org}/${key}`,db=store(root);
@@ -121,16 +94,6 @@ export function declareAgentIntent({root,org,server,profiles,profile,key,machine
121
94
  return {id:row.id,key:row.key,baseKey:stable,declared:!!row.declared,intended:refresh?intended:JSON.parse(row.profile_json),fresh,org};
122
95
  }finally{db.close();}
123
96
  }
124
- function adoptAgentIntent(root,intent,identity){
125
- const db=store(root);try{
126
- db.exec('BEGIN IMMEDIATE');
127
- const existing=db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(intent.org,identity.id);
128
- 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());
129
- db.prepare('UPDATE agent_declaration_heads SET agent_id=? WHERE org=? AND key=?').run(identity.id,intent.org,intent.baseKey);
130
- db.exec('COMMIT');
131
- return {...intent,id:identity.id,key:existing?.key??`${intent.baseKey}/adopted/${identity.id}`,declared:true};
132
- }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
133
- }
134
97
  function nextAgentGeneration(root,intent,machine){
135
98
  const db=store(root);try{
136
99
  db.exec('BEGIN IMMEDIATE');
@@ -145,125 +108,6 @@ function nextAgentGeneration(root,intent,machine){
145
108
  db.exec('COMMIT');return {...intent,id,key,declared:false};
146
109
  }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
147
110
  }
148
- function synchronizeIntent(intent,{root,context,body}) {
149
- let identity;
150
- if(intent.declared)identity=agentDoor('get',[intent.id],{context}).data;
151
- else {
152
- try{identity=agentDoor('declare',[{...body,id:intent.id},digest(intent.key)],{context}).data;}
153
- catch(error){if(error.code==='idempotency_conflict')identity=agentDoor('get',[intent.id],{context}).data;else throw error;}
154
- identity=agentDoor('get',[intent.id],{context}).data;
155
- 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();}
156
- }
157
- if(identity.state!=='active')throw new Error(`agent ${identity.path} is ${identity.state}: ${identity.suspension_reason??identity.end_reason??'unavailable'}`);
158
- if(body.profile!==identity.profile)identity=agentDoor('profile',[identity.id,body.profile,identity.revision],{context}).data;
159
- if(body.parent!==undefined&&body.parent!==identity.parent)identity=agentDoor('reparent',[identity.id,body.parent,identity.revision],{context}).data;
160
- return identity;
161
- }
162
- export function ensureRootAgent(options) {
163
- const {root,org,context,profile=options.profiles.default?'default':null}=options;
164
- if(!profile)throw new Error('declare the Org root with a named profile before launching card agents');
165
- let intent=declareAgentIntent({...options,profile,key:'root'});
166
- let identity;
167
- try{identity=agentDoor('root',[],{context}).data;}
168
- catch(error){
169
- if(error.status!==404)throw error;
170
- try{identity=synchronizeIntent(intent,{root,context,body:{name:basename(root),profile,root:true}});}
171
- catch(conflict){if(conflict.status!==409)throw conflict;identity=agentDoor('root',[],{context}).data;}
172
- }
173
- if(identity.state==='ended'){
174
- if(intent.id!==identity.id)intent=adoptAgentIntent(root,intent,identity);
175
- intent=nextAgentGeneration(root,intent,options.machine);
176
- identity=synchronizeIntent(intent,{root,context,body:{name:identity.name,profile,root:true}});
177
- }
178
- if(intent.id!==identity.id)adoptAgentIntent(root,intent,identity);
179
- // Another admitted manager may have declared the root first; its server id remains authoritative.
180
- if(identity.state!=='active')throw new Error('the Org root agent is not active');
181
- return identity;
182
- }
183
- /** Cold starts and relaunches use the same local intent and public declaration. Reviewer and implementer keys differ. */
184
- export function ensureCardAgent(options) {
185
- const {root,org,profiles,profile,card,role,sponsor=null,context}=options;
186
- const assigned=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
187
- // A locally minted agent that has not reached Teams yet is declared below under the same key and id.
188
- if(assigned&&agentIntent(root,org,assigned.agent_id)?.declared!==0) {
189
- const identity=agentDoor('get',[assigned.agent_id],{context}).data;
190
- if(identity.state==='active'){
191
- const intended=resolveAgentProfile(profiles,identity.profile);
192
- 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();}
193
- return {identity,intended,assignment:assigned.id,
194
- assignments:activeAgentAssignments(root).filter(item=>item.org===org&&item.agent_id===identity.id),org};
195
- }
196
- if(identity.state!=='ended')throw new Error(`card's assigned agent is ${identity.state}`);
197
- finishAgentAssignment(root,org,assigned.id,`agent ${identity.id} is ended`);
198
- }
199
- let intent=declareAgentIntent({...options,key:`card/${card}/${role}`});
200
- const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
201
- if(!intent.declared){
202
- const existing=agentDoor('find',[`${card}-${role}`,rootAgent.id],{context}).data;
203
- if(existing){
204
- intent=adoptAgentIntent(root,intent,existing);
205
- }
206
- }
207
- if(intent.declared&&agentDoor('get',[intent.id],{context}).data.state==='ended')intent=nextAgentGeneration(root,intent,options.machine);
208
- const identity=synchronizeIntent(intent,{root,context,body:{name:`${card}-${role}`,parent:rootAgent.id,sponsor,profile,...(options.starterSession?{starter_session:options.starterSession}:{})}});
209
- const assignment=assignAgent(root,org,identity.id,{kind:'card',target:card,role});
210
- return {identity,intended:intent.intended,assignment,assignments:[{kind:'card',target:card,role}],org};
211
- }
212
- /** Starting a named profile declares its standing agent; bindings are sessions of that identity. */
213
- export function ensureProfileAgent(options){
214
- const {root,org,profiles,profile,context,agent:declared=null,declarations={}}=options;
215
- if(declared&&declared!==profile){
216
- // A declared agent (the IR's `agents`) runs as a shared profile under its own identity and parent.
217
- const decl=declarations[declared]??{};
218
- const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
219
- const parent=decl.parent?ensureProfileAgent({...options,profile:declarations[decl.parent]?.profile??decl.parent,agent:decl.parent}).identity:rootAgent;
220
- let declaration=declareAgentIntent({...options,key:`agent/${declared}`});
221
- if(!declaration.declared){
222
- const existing=agentDoor('find',[declared,parent.id],{context}).data;
223
- if(existing)declaration=adoptAgentIntent(root,declaration,existing);
224
- }
225
- if(declaration.declared&&agentDoor('get',[declaration.id],{context}).data.state==='ended')declaration=nextAgentGeneration(root,declaration,options.machine);
226
- const identity=synchronizeIntent(declaration,{root,context,body:{name:declared,parent:parent.id,profile,...(decl.sponsor?{sponsor:decl.sponsor}:{})}});
227
- const assignment=assignAgent(root,org,identity.id,{kind:'agent',target:declared,role:'declared'});
228
- return {identity,intended:declaration.intended,assignment,assignments:[{kind:'agent',target:declared,role:'declared'}],org};
229
- }
230
- const rootAgent=ensureRootAgent({...options,profile:profiles.default?'default':profile});
231
- let identity=rootAgent,intent;
232
- if(profile===(profiles.default?'default':profile))intent=agentIntent(root,org,identity.id)?.profile;
233
- else{
234
- // Profile ancestry and agent parentage are the same intended narrowing tree.
235
- // Resolve first to reject a cycle before declaring any parent along it.
236
- resolveAgentProfile(profiles,profile);
237
- const parentProfile=profiles[profile].parent;
238
- const parent=parentProfile?ensureProfileAgent({...options,profile:parentProfile}).identity:rootAgent;
239
- let declaration=declareAgentIntent({...options,key:`profile/${profile}`});
240
- if(!declaration.declared){
241
- const existing=agentDoor('find',[profile,parent.id],{context}).data;
242
- if(existing)declaration=adoptAgentIntent(root,declaration,existing);
243
- }
244
- if(declaration.declared&&agentDoor('get',[declaration.id],{context}).data.state==='ended')declaration=nextAgentGeneration(root,declaration,options.machine);
245
- identity=synchronizeIntent(declaration,{root,context,body:{name:profile,parent:parent.id,profile}});
246
- intent=declaration.intended;
247
- }
248
- const assignment=assignAgent(root,org,identity.id,{kind:'profile',target:profile,role:'profile'});
249
- return {identity,intended:intent??resolveAgentProfile(profiles,profile),assignment,assignments:[{kind:'profile',target:profile,role:'profile'}],org};
250
- }
251
- /** An explicit fork declares a child; a routine helper launch keeps its starter's identity. */
252
- export function ensureForkAgent(options) {
253
- const {root,org,profiles,parent,session,name,key,context}=options;
254
- const managed=agentDoor('management',[parent],{context}).data;
255
- const source=agentDoor('session',[session],{context}).data;
256
- if(source.agent?.id!==parent)throw new Error('fork source is not owned by the declared parent');
257
- const parentIntent=agentIntent(root,org,parent)?.profile??resolveAgentProfile(profiles,managed.profile);
258
- let intent=declareAgentIntent({...options,profile:managed.profile,key:`fork/${parent}/${key}`,refresh:false});
259
- if(intent.declared&&agentDoor('get',[intent.id],{context}).data.state==='ended')throw new Error('this fork identity ended; declare a new fork key');
260
- // The snapshot copies configuration only. Memory stays the new child's own empty state.
261
- const intended=structuredClone(intent.fresh?parentIntent:intent.intended);
262
- 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();}
263
- const identity=synchronizeIntent(intent,{root,context,body:{name,parent,profile:managed.profile,starter_session:session}});
264
- const assignment=assignAgent(root,org,identity.id,{kind:'fork',target:session,role:key});
265
- return {identity,intended,assignment,assignments:[{kind:'fork',target:session,role:key}],org};
266
- }
267
111
  export function agentRunForKey(root,org,agent,key) {
268
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();}
269
113
  }
@@ -272,20 +116,20 @@ const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SE
272
116
  function recordDeclaration(root,org,id,declaration){
273
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();}
274
118
  }
275
- /** The identity a run starts as without asking Teams: the daemon-minted id and retained intent. An identity not yet
276
- * declared is declared later under the same id (synchronizeLocalAgents). Nothing is granted from this local record;
277
- * `identity.synced` says whether Teams has the declaration, and the record's own `synced: false` that Teams was not read. */
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. */
278
122
  function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
279
123
  const {root,org,server,profiles,profile,machine=null}=options;
280
124
  const rootProfile=profiles.default?'default':profile;
281
125
  const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
282
- recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
126
+ recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile,name:basename(root),parent:null});
283
127
  let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
284
- // An agent Teams ended is never started again: its end, once synchronized, is on its runs here (synchronizeAgentEnds),
285
- // and the key's next generation is minted in its place, with fresh memory.
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.
286
130
  if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
287
- if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
288
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});
289
133
  const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
290
134
  lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
291
135
  synced:!!intent.declared,main_session:null,revision:0};
@@ -356,38 +200,61 @@ function adoptLocalOrg(root,scope){
356
200
  db.exec('COMMIT');
357
201
  }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
358
202
  }
359
- const DECLARE_ATTEMPTS=6;
360
- // A sync claim outlasts one Teams call chain (each door call bounded at 25-60 s).
361
- const SYNC_CLAIM_MS=600000;
362
- /** Declare every locally minted identity once Teams is reachable, under the id its sessions already carry. */
363
- export function synchronizeLocalAgents({root,profiles,context,machine=null,declarations={}}){
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}={}){
364
228
  const scope=agentScope(context);if(scope.offline)return [];
365
229
  adoptLocalOrg(root,scope);
366
- const db=store(root);let rows;
367
- try{rows=db.prepare('SELECT * FROM agent_intents WHERE org=? AND declared=0 AND declaration_json IS NOT NULL AND declare_failed_at IS NULL AND declare_next_at<=? ORDER BY created_at').all(scope.org,Date.now());}finally{db.close();}
368
- const pending=[],base={root,org:scope.org,server:scope.server,profiles,context,machine};
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();}
369
236
  for(const row of rows){
370
237
  const declaration=JSON.parse(row.declaration_json);
371
- // claimed by compare-and-set on its own next attempt time, set ahead only while it is due: a second pass skips it
372
- const claim=store(root);let claimed;
373
- try{claimed=claim.prepare('UPDATE agent_intents SET declare_next_at=? WHERE org=? AND id=? AND declared=0 AND declare_failed_at IS NULL AND declare_next_at<=?').run(Date.now()+SYNC_CLAIM_MS,row.org,row.id,Date.now()).changes===1;}finally{claim.close();}
374
- if(!claimed)continue;
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);
375
252
  try{
376
- if(declaration.kind==='root')ensureRootAgent({...base,profile:declaration.profile});
377
- else if(declaration.kind==='card')ensureCardAgent({...base,profile:declaration.profile,card:declaration.card,role:declaration.role,sponsor:declaration.sponsor,starterSession:declaration.starterSession});
378
- else if(declaration.kind==='profile')ensureProfileAgent({...base,profile:declaration.profile});
379
- else if(declaration.kind==='declared')ensureProfileAgent({...base,profile:declaration.profile,agent:declaration.agent,declarations});
380
- }catch(error){
381
- // Teams unreachable is no answer about this declaration: the rest wait for the next pass, and no attempt is counted.
382
- 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}];}
383
- // A refusal is this declaration's own: asked again after a doubling wait (one minute, at most an hour), and after
384
- // DECLARE_ATTEMPTS refusals left failed, said once, and never asked again.
385
- const made=Number(row.declare_attempts??0)+1,failed=made>=DECLARE_ATTEMPTS,db=store(root);
386
- 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();}
387
- pending.push({agent:row.id,error:error.message,attempts:made,failed});
388
- }
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();}
389
256
  }
390
- return pending;
257
+ return waiting;
391
258
  }
392
259
  /** A managed runtime may report its native id after opening. Keep the lease pointer out of the catalogue. */
393
260
  export function recordNativeAgentSession(root,runtimeRef,session) {
@@ -1,5 +1,7 @@
1
- // Consume the card-creation intent after the board transaction; retries reuse the daemon-minted id.
2
- import { agentScope, teamsUnavailable, ensureCardAgent, localCardAgent, finishAgentAssignment } from '../agents.mjs';
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
- // Without a reachable Teams the role's identity is minted here; the board's reconcile declares it later.
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
- tx(board,db=>event(db,effect.id,'agent_declared',{agent:agent.identity.id,org:scope.org,role:effect.role,profile,assignment:agent.assignment,synced:agent.synced!==false}));
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
+ }
@@ -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, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
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';
@@ -1302,15 +1302,14 @@ export function reconcileCallerLaunch(board, sid) {
1302
1302
  }
1303
1303
  }
1304
1304
 
1305
- /** The board's agent records: each run kept in the agent store, identities minted offline declared, ends, and usage.
1306
- * Teams I/O, so it runs in the delivery pass (`deliver`), never in a dispatch round. A run's session is bound to its
1307
- * agent by the machine that runs it, from the agent its launch named (the connector's bindAgentSessions), never here. */
1308
- function synchronizeAgents(board, report, entry) {
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) {
1309
1309
  // A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
1310
1310
  // A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
1311
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())
1312
1312
  .map(row=>{const session=json(row.metadata,{})?.supercode;return [session?.session_id?addressOf(session):`run:${row.id}`,row];})).values()];
1313
- const orgs=new Map();
1314
1313
  for(const row of agentRuns){
1315
1314
  const session=json(row.metadata,{})?.supercode;
1316
1315
  if(!session?.session_id||!session.agent_org)continue;
@@ -1330,16 +1329,7 @@ function synchronizeAgents(board, report, entry) {
1330
1329
  // One run whose session another agent already owns is that card's problem, said on it; the round goes on for the rest.
1331
1330
  try{recordAgentRun(board.root,session.agent_org,session.agent_id,{session:ref,machine:session.machine,run:row.id});}
1332
1331
  catch(error){report.queued.push([row.task_id,`run ${row.id}: ${error.message}`]);continue;}
1333
- orgs.set(session.agent_org,session.agent_context);
1334
1332
  }
1335
- // Identities minted while Teams was absent are declared under their ids before their sessions are joined.
1336
- const context=process.env.SUPERCODE_CONTEXT;
1337
- let scope;try{scope=agentScope(context);}catch(error){scope={offline:true};report.failed.push(['agents',`agent declarations wait: ${error.message}`]);}
1338
- if(!scope.offline){
1339
- orgs.set(scope.org,orgs.get(scope.org)??context);
1340
- 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}`]);
1341
- }
1342
- orgs.delete('local');
1343
1333
  // G3: an ended run's session usage, read once from its harness's own records on its machine
1344
1334
  for(const org of new Set(agentRuns.map(row=>json(row.metadata,{})?.supercode?.agent_org).filter(Boolean))){
1345
1335
  let read=0;
@@ -1361,7 +1351,9 @@ function synchronizeAgents(board, report, entry) {
1361
1351
  const task=getTaskNow(board,assignment.target);
1362
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}`);
1363
1353
  }
1364
- for(const [org,context] of orgs)for(const pending of synchronizeAgentEnds(board.root,org,{context}))report.queued.push([pending.agent,`agent end synchronization pending: ${pending.error}`]);
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}`]);}
1365
1357
  }
1366
1358
 
1367
1359
  function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
@@ -2657,7 +2649,7 @@ export async function deliver({ root, slugs = null }) {
2657
2649
  tx(board, (db) => queueBoardMail(db)); phase('mail');
2658
2650
  report.effects = await runEffects(board, [], { pending: true }); phase('effects');
2659
2651
  await notify(board, fleet, report, entry); phase('notify');
2660
- synchronizeAgents(board, report, entry); phase('agents');
2652
+ synchronizeAgents(board, report); phase('agents');
2661
2653
  report.effects_failed = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NOT NULL').get().n);
2662
2654
  report.effects_waiting = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NULL').get().n);
2663
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,30 +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
120
  export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
126
- 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;
127
- export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
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>;
128
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. */
129
124
  export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
130
- export function teamsUnavailable(error: unknown): boolean;
131
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. */
132
- export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
133
- export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
134
- export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
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;
135
134
  export function localAgentSessions(root: Path, org: string, id: string): Array<{session: string; machine: string | null; ended_at: number | null; end_reason: string | null}>;
136
- export function synchronizeLocalAgents(options: {root: Path; profiles: Record<string, Profile>; context?: string; machine?: string | null}): Array<{agent: string; error: string}>;
137
135
  export function agentProfileMismatch(intent: unknown, resolved: unknown): import('@volter/teams/agent-profile').ProfileComparison;
138
136
 
139
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, teamsUnavailable, localCardAgent, localProfileAgent, localAgentRecord, localAgentSessions, synchronizeLocalAgents, declareAgentIntent, ensureRootAgent, ensureCardAgent,ensureProfileAgent,ensureForkAgent,agentRunForKey, agentIntent, assignAgent, reassignAgent,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds,recordAgentRun, recordNativeAgentSession, agentRunForSession,agentPreamble, agentEnvironment, agentProfileMismatch } from "./agents.mjs";
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/loop.mjs CHANGED
@@ -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 { agentDoor,agentScope,teamsUnavailable,ensureProfileAgent,localProfileAgent,localAgentRecord,localAgentSessions,synchronizeLocalAgents,ensureForkAgent,agentRunForKey,agentIntent,agentPreamble,agentEnvironment,recordAgentRun,recordNativeAgentSession,agentRunForSession } from './agents.mjs';
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';
@@ -194,14 +194,12 @@ export class Orchestrator {
194
194
  );
195
195
  }
196
196
 
197
- /** Identities minted while Teams was absent are declared under their ids. Their sessions are bound to them by the
198
- * machine that runs them, from the agent each launch named (the connector's bindAgentSessions), never here. */
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
199
  declareLocalAgents(){
200
200
  if(!existsSync(join(this.state.root,'agent-state.sqlite')))return;
201
- try{
202
- const context=process.env.SUPERCODE_CONTEXT;
203
- 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}`);
204
- }catch(error){this.log('warn',`agent declaration: ${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}`);}
205
203
  }
206
204
 
207
205
  /**
@@ -418,42 +416,21 @@ export class Orchestrator {
418
416
  materializeSkills(profile);
419
417
  let launch,agent,scope,context,agentPrompt,main,agentHome;
420
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.
421
422
  context=process.env.SUPERCODE_CONTEXT;scope=agentScope(context);
422
- const local=error=>{if(!teamsUnavailable(error))throw error;this.log('warn',`agent identity for ${profileName} stays local: ${error.message}`);};
423
- let previous=null,reader=null;
423
+ let previous=null;
424
424
  if(agent_as) {
425
- // A launch as a named agent is an act Teams authorizes; it never proceeds on local intent alone.
426
- agentDoor('management',[agent_as],{context});
427
- const identity=agentDoor('get',[agent_as],{context}).data,intent=agentIntent(this.state.root,scope.org,agent_as);
428
- if(!intent)throw new Error('launch as an agent requires its retained profile intent');
429
- agent={identity,intended:intent.profile,assignments:intent.assignments,org:scope.org};
430
- } else if(!scope.offline) {
431
- 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;
432
428
  }
433
- // Situation 16: no reachable Teams. The locally minted identity runs now and is declared later under its id.
434
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}`);
435
431
  agent.intended.resolved.worker={...agent.intended.resolved.worker,...worker,cwd};
436
432
  main=new_fork||!agent.identity.main_session;
437
- if(agent.synced!==false){
438
- try{
439
- previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
440
- if(resume_runtime_id&&!new_fork) {
441
- // a session no sync rule publishes is unknown to Teams: its owner is the local run record, read below
442
- let resumed=null;
443
- try{resumed=agentDoor('session',[`${worker.harness}:${resume_runtime_id}`],{context}).data;}catch(error){if(error.code!=='not_found')throw error;}
444
- if(resumed){
445
- if(resumed.agent?.id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
446
- main=resumed.main;
447
- } else {
448
- const owner=agentRunForSession(this.state.root,[`${worker.harness}:${resume_runtime_id}`]);
449
- if(owner&&owner.agent_id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
450
- if(owner)main=owner.main===1;
451
- }
452
- }
453
- reader=agentDoor('readerContext',[agent.identity.id,null],{context,timeout:60000});
454
- }catch(error){local(error);}
455
- }
456
- if(resume_runtime_id&&!new_fork&&!previous){
433
+ if(resume_runtime_id&&!new_fork){
457
434
  const owner=agentRunForSession(this.state.root,[`${worker.harness}:${resume_runtime_id}`]);
458
435
  if(owner&&owner.agent_id!==agent.identity.id)throw new Error('the resumed session is not owned by this profile agent');
459
436
  if(owner)main=owner.main===1;
@@ -468,7 +445,7 @@ export class Orchestrator {
468
445
  const route = workerRoute(profile, worker, this.vault);
469
446
  // a worker of any harness but Hermes gets Hermes's CLI against its profile's home (row 7)
470
447
  const tools = worker.harness === 'hermes' ? {} : hermesToolEnv(agentHome);
471
- const env = { ...tools, ...activationEnv({ root: this.state.root, profile, surfaceKey: keyString, vault: this.vault }), ...route.env,...agentEnvironment(agent,{root:this.state.root,context:reader?.context,main}) };
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}) };
472
449
  env.HERMES_HOME=agentHome;
473
450
  declareWorkerHome({ harness: worker.harness, env, cwd, log: (level, message) => this.log(level, message) });
474
451
  // All sessions of this agent carry its state; a shared profile contributes configuration only.
@@ -537,8 +514,9 @@ export class Orchestrator {
537
514
  if(!binding||binding.ended_at)throw new Error('register the fork target conversation before starting its runtime');
538
515
  if(this.runtimes.has(leaseId(profileName,keyString)))throw new Error('fork requires an unoccupied target conversation');
539
516
  if(!(await this.descriptorFor(locator.harness))?.runtime?.capabilities?.resume_session)throw new Error('this harness cannot resume a materialized fork');
540
- const context=process.env.SUPERCODE_CONTEXT,scope=agentDoor('scope',[],{context}),session=`${locator.harness}:${locator.session_id}`;
541
- const child=ensureForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key,context});
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}`);
542
520
  const runKey=`fork:${key}`,prior=agentRunForKey(this.state.root,scope.org,child.identity.id,runKey);
543
521
  let target;
544
522
  if(prior) {
@@ -580,19 +558,10 @@ export class Orchestrator {
580
558
  let preamble=null;
581
559
  if(owner) {
582
560
  try {
583
- const context=process.env.SUPERCODE_CONTEXT;
584
- const intent=agentIntent(this.state.root,owner.org,owner.agent_id);
585
- if(!intent)throw new Error('retained session has no agent profile intent');
586
- try{
587
- const live=agentDoor('session',[owner.session_ref],{context}).data;
588
- 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');
589
- const previous=agentDoor('sessions',[owner.agent_id,{limit:200}],{context}).items??[];
590
- preamble=agentPreamble({identity:live.agent,intended:intent.profile,assignments:intent.assignments},{main:live.main,previous});
591
- }catch(error){
592
- // Teams cannot be asked: the session continues as the agent this home recorded, never as another.
593
- if(!teamsUnavailable(error))throw error;
594
- 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)});
595
- }
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)});
596
565
  }catch(error){return interrupted(`agent identity unavailable: ${error.message}`);}
597
566
  }
598
567
  let capable = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.43",
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.15",
28
+ "@volter/teams": "0.2.16",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },