@volter/supercode-orchestrator 0.5.12 → 0.5.14

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
@@ -9,8 +9,10 @@ import { runtimeDoor } from './board/runtime-door.mjs';
9
9
  import { resolveAgentProfile } from './agent-profiles.mjs';
10
10
  import { compareAgentProfile } from '@volter/teams/agent-profile';
11
11
  const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
12
+ // The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
13
+ 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);
12
14
  export function agentDoor(op,args=[],{context,timeout=25000}={}){
13
- const reply=spawnSync(process.execPath,[entry],{input:JSON.stringify({op,args,context}),encoding:'utf8',timeout,maxBuffer:4*1024*1024});
15
+ 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}`}});
14
16
  if(reply.status!==0){let detail;try{detail=JSON.parse(reply.stderr);}catch{detail={message:reply.error?.message??String(reply.stderr).slice(-1000)};}throw Object.assign(new Error(detail.message??'Teams agent door unavailable'),detail);}
15
17
  return JSON.parse(reply.stdout);
16
18
  }
@@ -365,8 +367,23 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
365
367
  return agentRunForSession(root,[runtimeRef]);
366
368
  }
367
369
  /** Join an observed native session through the public catalogue; unknown catalogue state remains pending. */
370
+ // A run whose session ownership Teams refused (a 4xx answer to the claim: not found, forbidden, conflict, signed out)
371
+ // is asked again after a growing wait (two minutes, doubling to an hour), not every round: each ask spends the shared
372
+ // credential's allowance. Anything else (the rate limit, a timeout, a 5xx, the local daemon's bind of a session just
373
+ // spawned) is not the run's own and is asked again next round.
374
+ const runRetry=new Map();
375
+ const retryKey=(org,row)=>`${org}/${row.agent_id}/${row.session_ref}`;
376
+ const refused=error=>error?.status>=400&&error.status<500&&error.status!==408&&error.status!==429;
368
377
  export function synchronizeAgentRun(root,org,row,{context,intended}={}) {
369
- const response=agentDoor('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});
378
+ try{const answer=joinAgentRun(root,org,row,{context,intended});runRetry.delete(retryKey(org,row));return answer;}
379
+ catch(error){
380
+ if(error?.claimRefused){const wait=Math.min((runRetry.get(retryKey(org,row))?.wait??60000)*2,3600000);runRetry.set(retryKey(org,row),{at:Date.now()+wait,wait});}
381
+ throw error;
382
+ }
383
+ }
384
+ function joinAgentRun(root,org,row,{context,intended}) {
385
+ const claim=(...args)=>{try{return agentDoor(...args);}catch(error){throw Object.assign(error,{claimRefused:refused(error)});}};
386
+ 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});
370
387
  const intent=agentIntent(root,org,row.agent_id);
371
388
  const runtime=agentDoor('bindContext',[row.agent_id,row.session_ref,row.machine,intent?.assignments??[],intended??intent?.profile.resolved],{context});
372
389
  publishAgentRun(root,org,row.session_ref);
@@ -381,7 +398,7 @@ export function recordAgentRun(root,org,id,{session,machine=null,run,profile=nul
381
398
  db.exec('COMMIT');
382
399
  }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
383
400
  }
384
- export function pendingAgentRuns(root,org){const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND published=0 ORDER BY rowid LIMIT 100').all(org);}finally{db.close();}}
401
+ export function pendingAgentRuns(root,org){const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND published=0 ORDER BY rowid LIMIT 100').all(org).filter(row=>!(runRetry.get(retryKey(org,row))?.at>Date.now()));}finally{db.close();}}
385
402
  /** Session references come from the bound runtime, never from a model's requested agent id. */
386
403
  export function agentRunForSession(root, references) {
387
404
  const db = store(root);
@@ -3,8 +3,12 @@
3
3
  import { fromContext } from '@volter/teams/context';
4
4
  import { supercodeHome } from '@volter/teams/home';
5
5
  import { createHash } from 'node:crypto';
6
- import { openMachine } from '@volter/teams/machine-door';
6
+ import { openMachine, normalMachineName } from '@volter/teams/machine-door';
7
+ import { hostname } from 'node:os';
7
8
  import { StreamRpc } from '@volter/teams/client';
9
+ // This machine is reached through its own daemon's socket, never through the Teams server: the server route lists the
10
+ // org's machines and opens a channel on the shared credential's allowance for a request that never leaves the box.
11
+ const reach=machine=>machine&&normalMachineName(machine)!==normalMachineName(hostname())?machine:null;
8
12
  let input='';for await(const chunk of process.stdin)input+=chunk;
9
13
  try{
10
14
  const {op,args=[],context}=JSON.parse(input);
@@ -28,14 +32,14 @@ try{
28
32
  else if(op==='reader')result=await team.agents.delegateReader(args[0],{},{...options,ifMatch:args[1]});
29
33
  else if(op==='bindContext'){
30
34
  const key=createHash('sha256').update(`${scope.origin}/${scope.team_id}/${args[0]}`).digest('hex');
31
- const channel=await openMachine({machine:args[2]??null,context:context??process.env.SUPERCODE_CONTEXT});
35
+ const channel=await openMachine({machine:reach(args[2]),context:context??process.env.SUPERCODE_CONTEXT});
32
36
  try{const rpc=new StreamRpc(await channel.open('node'));result=await rpc.call('harness.v1.agents.bindSession',{session:args[1],context:`agent-${key.slice(0,32)}`,assignments:args[3]??[],intended:args[4]??null});}
33
37
  finally{channel.close();}
34
38
  }
35
39
  else if(op==='readerContext'){
36
40
  const identity=await team.agents.get(args[0],options);
37
41
  const reader=await team.agents.delegateReader(args[0],{expires_in_ms:86400000},{...options,ifMatch:identity.data.revision});
38
- const channel=await openMachine({machine:args[1]??null,context:context??process.env.SUPERCODE_CONTEXT});
42
+ const channel=await openMachine({machine:reach(args[1]),context:context??process.env.SUPERCODE_CONTEXT});
39
43
  try{const rpc=new StreamRpc(await channel.open('node'));result=await rpc.call('harness.v1.agents.context',{origin:scope.origin,team:scope.team_id,agent:args[0],credential:reader.data});}
40
44
  finally{channel.close();}
41
45
  }
@@ -1189,7 +1189,7 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1189
1189
  for(const [org,context] of orgs)for(const row of pendingAgentRuns(board.root,org)){
1190
1190
  const launch=agentRuns.find(run=>String(run.id)===String(row.run_id));
1191
1191
  try{synchronizeAgentRun(board.root,org,row,{context,intended:json(launch?.metadata,{})?.supercode?.agent_profile_intent});}
1192
- catch(error){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);}
1192
+ catch(error){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);if(error.code==='rate_limited')break;}
1193
1193
  }
1194
1194
  const deferred=view(board,db=>db.prepare("SELECT t.id,r.started_at FROM tasks t JOIN task_runs r ON r.id=t.current_run_id WHERE r.ended_at IS NULL AND json_extract(r.metadata,'$.supercode.launch_deferred')=1").all());
1195
1195
  // A deferred launch was never issued (a reading or door was not yet available): it is the start its manager, creator
@@ -6,7 +6,7 @@ import { boardSourceName, publishableCard, recordCardPublications } from './publ
6
6
  import { listBoards, openBoard } from './store.mjs';
7
7
 
8
8
  export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
9
- let work=null,dirty=false,closed=false,retry=null,reconcile=true;const sources=new Map();
9
+ let work=null,dirty=false,closed=false,retry=null,reconcile=true,heldUntil=0;const sources=new Map();
10
10
  const run=async()=>{
11
11
  if(reconcile){
12
12
  // Establish every native source head before any network operation. A
@@ -37,10 +37,14 @@ export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
37
37
  };
38
38
  const wake=(refresh=true)=>{
39
39
  reconcile ||= refresh;dirty=true;if(work||closed)return work;
40
+ // Refused for the rate limit, delivery waits out the server's retry-after: a board write meanwhile would only ask
41
+ // again and spend the allowance the credential shares with every other caller.
42
+ if(Date.now()<heldUntil)return null;
40
43
  work=(async()=>{while(dirty&&!closed){dirty=false;await run();}})().catch(error=>{
41
44
  log(`board event delivery pending: ${error.code?`${error.code}: `:''}${error.message}`);
42
45
  // Retry durable delivery, not a read of the board's current state.
43
- retry=setTimeout(()=>wake(false),5000);retry.unref?.();
46
+ const wait=error.code==='rate_limited'?Math.max(5000,Number(error.details?.retry_after_seconds??30)*1000):5000;heldUntil=Date.now()+wait;
47
+ clearTimeout(retry);retry=setTimeout(()=>{heldUntil=0;wake(false);},wait);retry.unref?.();
44
48
  }).finally(()=>{work=null;});return work;
45
49
  };
46
50
  return {wake,close(){closed=true;clearTimeout(retry);}};
@@ -15,14 +15,18 @@ const RESETTLE_MS = 30_000;
15
15
  export async function* followBoardPublications({root,after,signal}) {
16
16
  const home=identity(root), positions={}, watchers=new Map();
17
17
  let dirty=true, wake=null, failure=null, inventory=null;
18
- // The stores' files as this follower last left them. Its own pass writes (the publication transaction) and closes
19
- // the board, which rewrites the WAL and raises a watch event of its own; an event that finds every store file as the
20
- // pass left it is that echo, not a change, and does not start another pass. Without this the follower woke itself
21
- // every half second and burned a core with nothing to publish (t_0328d520).
22
- let stores=[], settled=null;
23
- const stamp=()=>stores.map(path=>{try{const s=statSync(path);return `${s.size}:${s.mtimeMs}`;}catch{return '-';}}).join('|');
18
+ // Each store file as this follower's own pass left it, stamped right after that board's close. Its pass writes (the
19
+ // publication transaction) and closes the board, which rewrites the WAL and raises a watch event of its own; an event
20
+ // for a store file that is still as its pass left it is that echo, not a change, and does not start another pass.
21
+ // Without this the follower woke itself every quarter second and burned a core with nothing to publish
22
+ // (t_0328d520). Stamped per board, a write to one board while later boards' passes run is never taken for an echo.
23
+ let stores=[];
24
+ const settled=new Map();
25
+ const statOf=path=>{try{const s=statSync(path);return `${s.size}:${s.mtimeMs}`;}catch{return '-';}};
24
26
  const notify=(name=null)=>{
25
- if(name!==null&&settled!==null&&stores.some(path=>basename(path)===name)&&stamp()===settled)return;
27
+ const named=name===null?[]:stores.filter(path=>basename(path)===name);
28
+ // A store file no pass stamps (the root's kanban.db where only named boards exist) reads '-' until one does.
29
+ if(named.length&&named.every(path=>statOf(path)===(settled.get(path)??'-')))return;
26
30
  dirty=true;wake?.();
27
31
  };
28
32
  const directory=(path,accept)=>{
@@ -90,12 +94,11 @@ export async function* followBoardPublications({root,after,signal}) {
90
94
  for(const row of rows){positions[slug].sequence=row.sequence;yield {type:'event',id:row.id,kind:row.kind,cursor:cursorOf(home,positions),data:{board:slug,card:JSON.parse(row.snapshot)}};}
91
95
  }
92
96
  }
93
- }finally{board.close();}
97
+ }finally{board.close();const paths=backingPaths(root,slug);for(const path of [paths.sqlite,`${paths.sqlite}-wal`])settled.set(path,statOf(path));}
94
98
  }
95
99
  if(initial){initial=false;yield {type:'snapshot_end',cursor:cursorOf(home,positions)};dirty=true;}
96
100
  else if(checkpointPending){checkpointPending=false;yield {type:'checkpoint',cursor:cursorOf(home,positions)};}
97
- settled=stamp();
98
- // A write that lands between the pass and the stamp above has its event taken for this pass's echo: a pass
101
+ // A write that lands between a board's close and its stamp has its event taken for that pass's echo: a pass
99
102
  // every RESETTLE_MS anyway bounds how long that write can wait.
100
103
  let timer=null;
101
104
  if(!dirty&&!signal?.aborted)await new Promise(done=>{wake=done;timer=setTimeout(()=>{dirty=true;done();},RESETTLE_MS);timer.unref?.();if(dirty||signal?.aborted)done();});
package/loop.mjs CHANGED
@@ -200,7 +200,7 @@ export class Orchestrator {
200
200
  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}`);
201
201
  for(const row of pendingAgentRuns(this.state.root,scope.org)){
202
202
  try{synchronizeAgentRun(this.state.root,scope.org,row,{context,intended:row.profile_json?JSON.parse(row.profile_json):undefined});}
203
- catch(error){this.log('warn',`agent session ${row.session_ref}: ${error.message}`);}
203
+ catch(error){this.log('warn',`agent session ${row.session_ref}: ${error.message}`);if(error.code==='rate_limited')break;}
204
204
  }
205
205
  }catch(error){this.log('warn',`agent session synchronization: ${error.message}`);}
206
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
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.41",
28
- "@volter/teams": "0.2.7",
28
+ "@volter/teams": "0.2.8",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },