@volter/supercode-orchestrator 0.5.36 → 0.5.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents.mjs CHANGED
@@ -8,6 +8,8 @@ import { createHash, randomBytes } from 'node:crypto';
8
8
  import { runtimeDoor } from './board/runtime-door.mjs';
9
9
  import { resolveAgentProfile } from './agent-profiles.mjs';
10
10
  import { compareAgentProfile } from '@volter/teams/agent-profile';
11
+ import { WorkspaceContexts } from '@volter/teams/context';
12
+ import { supercodeHome } from '@volter/teams/home';
11
13
  const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
12
14
  // The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
13
15
  const HOST_CALLER=(process.env.SUPERCODE_CALLER_NAME??`orchestrator ${process.argv.slice(2).filter(arg=>!arg.startsWith('-')).slice(0,2).filter(arg=>/^[a-z][a-z0-9-]{0,30}$/.test(arg)).join(' ')}`).slice(0,80);
@@ -22,11 +24,9 @@ export function teamsUnavailable(error){
22
24
  if(error?.status!=null)return error.status>=500||error.status===401;
23
25
  return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
24
26
  }
25
- /** The Org agent identity belongs to: the selected Teams context, or this home's local store when there is none. */
26
- export function agentScope(context){
27
- try{return agentDoor('scope',[],{context});}
28
- catch(error){if(teamsUnavailable(error))return {server:'local',org:'local',principal:null,offline:true,reason:error.message};throw error;}
29
- }
27
+ /** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
28
+ * or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
29
+ export function agentScope(context){return agentDoor('scope',[],{context});}
30
30
  function store(root){
31
31
  mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
32
32
  db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
@@ -260,27 +260,42 @@ export function ensureForkAgent(options) {
260
260
  export function agentRunForKey(root,org,agent,key) {
261
261
  const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND agent_id=? AND run_id=? ORDER BY rowid DESC LIMIT 1').get(org,agent,key)??null;}finally{db.close();}
262
262
  }
263
+ const declarationHead=(root,org,key)=>{const db=store(root);try{return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,key)?.agent_id??null;}finally{db.close();}};
264
+ const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
263
265
  function recordDeclaration(root,org,id,declaration){
264
266
  const db=store(root);try{db.prepare('UPDATE agent_intents SET declaration_json=? WHERE org=? AND id=? AND declared=0').run(JSON.stringify(declaration),org,id);}finally{db.close();}
265
267
  }
266
- /** Situation 16: no reachable Teams. The daemon-minted id and retained intent stand in for the declaration,
267
- * which synchronizeLocalAgents later makes under the same id. Nothing is granted from this local record. */
268
+ /** The identity a run starts as without asking Teams: the daemon-minted id and retained intent. An identity not yet
269
+ * declared is declared later under the same id (synchronizeLocalAgents). Nothing is granted from this local record;
270
+ * `identity.synced` says whether Teams has the declaration, and the record's own `synced: false` that Teams was not read. */
268
271
  function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
269
272
  const {root,org,server,profiles,profile,machine=null}=options;
270
273
  const rootProfile=profiles.default?'default':profile;
271
274
  const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
272
275
  recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
273
- const intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
276
+ let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
277
+ // An agent Teams ended is never started again: its end, once synchronized, is on its runs here (synchronizeAgentEnds),
278
+ // and the key's next generation is minted in its place, with fresh memory.
279
+ if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
274
280
  if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
275
281
  const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
276
282
  const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
277
283
  lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
278
- synced:false,main_session:null,revision:0};
284
+ synced:!!intent.declared,main_session:null,revision:0};
279
285
  const assigned=assignAgent(root,org,intent.id,assignment);
280
286
  return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
281
287
  }
282
288
  export function localCardAgent(options){
283
- const {card,role,profile}=options;
289
+ const {root,org,server,profiles,card,role,profile}=options;
290
+ // A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
291
+ // active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
292
+ const held=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
293
+ if(held&&held.agent_id!==declarationHead(root,org,`${server}/${org}/card/${card}/${role}`)&&!endedHere(root,org,held.agent_id)){
294
+ const record=localAgentRecord(root,org,held.agent_id);
295
+ const identity=record?{...record.identity,synced:true}:{id:held.agent_id,kind:'agent',org,name:held.agent_id,path:held.agent_id,parent:null,
296
+ lineage:{starter:null},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',synced:true,main_session:null,revision:0};
297
+ return {identity,intended:record?.intended??resolveAgentProfile(profiles,profile),assignment:held.id,assignments:[{kind:'card',target:card,role}],org,synced:false};
298
+ }
284
299
  return localAgent(options,{key:`card/${card}/${role}`,name:`${card}-${role}`,assignment:{kind:'card',target:card,role},
285
300
  declaration:{kind:'card',card,role,profile,sponsor:options.sponsor??null,starterSession:options.starterSession??null},
286
301
  starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
@@ -459,6 +474,13 @@ export function agentPreamble(agent,{main=true,previous=[]}={}){
459
474
  const {identity:a,intended,assignments=[]}=agent;
460
475
  return `You are agent ${a.path} (${a.id}), ${main?'its main session':'a concurrent session'}.${a.synced===false?' This identity was minted on this machine and is not yet declared on a Teams server; it keeps this id when it is.':''}\nSponsor: ${a.sponsor??'none yet'}; parent: ${a.parent??'none'}; lineage: ${JSON.stringify(a.lineage)}.\nAssignments: ${assignments.map(value=>`${value.kind} ${value.target}, role ${value.role}`).join("; ")}; profile: ${a.profile}; permission layers: ${JSON.stringify(intended.resolved.permissions.layers)}.\nAsk again with supercode agent whoami. List your sessions with supercode agent sessions; read a transcript only when needed.${previous.length?'\nEarlier sessions:\n'+previous.map(s=>`${s.session} · ${s.machine??'unknown machine'} · ended ${s.ended_at??'live'} · ${s.end_reason??''}`).join('\n'):''}`;
461
476
  }
477
+ /** The agent's reader context this home already holds (a session of it was bound here before), read from the contexts
478
+ * file without a request, or null. A launch on this machine hands it to the session, so its Teams verbs run delegated
479
+ * from the first turn; a context that has since expired fails there, as an absent one does. */
480
+ export function localReaderContext(org,id){
481
+ const contexts=new WorkspaceContexts(supercodeHome()).read().contexts??{};
482
+ return Object.entries(contexts).find(([,value])=>value?.principal_id===id&&value?.team_id===org)?.[0]??null;
483
+ }
462
484
  export function agentProfileMismatch(intent,resolved){return compareAgentProfile(intent?.resolved??intent,resolved);}
463
485
 
464
486
  export function agentEnvironment(agent,{root,context,session=null,main=true}={}) {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // The orchestrator's synchronous host reaches Teams through the installed neutral SDK.
3
- import { fromContext } from '@volter/teams/context';
3
+ import { fromContext, WorkspaceContexts } from '@volter/teams/context';
4
4
  import { supercodeHome } from '@volter/teams/home';
5
5
  import { openMachine, normalMachineName } from '@volter/teams/machine-door';
6
6
  import { hostname } from 'node:os';
@@ -11,6 +11,9 @@ const reach=machine=>machine&&normalMachineName(machine)!==normalMachineName(hos
11
11
  let input='';for await(const chunk of process.stdin)input+=chunk;
12
12
  try{
13
13
  const {op,args=[],context}=JSON.parse(input);
14
+ // No context selected: identity is this home's own (Org `local`). A context that is named but unreadable is an error.
15
+ const named=context??process.env.SUPERCODE_CONTEXT??new WorkspaceContexts(supercodeHome()).read().current;
16
+ if(op==='scope'&&(!named||named==='local')){process.stdout.write(JSON.stringify({server:'local',org:'local',principal:null,offline:true,reason:'no Teams context is selected'})+'\n');process.exit(0);}
14
17
  const {client,context:scope}=fromContext({supercodeHome:supercodeHome(),name:context??process.env.SUPERCODE_CONTEXT});
15
18
  const team=client.team(scope.team_id),options={signal:AbortSignal.timeout(20000)};
16
19
  let result;
@@ -20,7 +20,7 @@ import { readEventFleet } from './event-fleet.mjs';
20
20
  let eventFleetRoot = null;
21
21
  import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
22
22
  import { runtimeDoor } from './runtime-door.mjs';
23
- import { agentDoor, agentScope, teamsUnavailable, ensureCardAgent, localCardAgent, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
23
+ import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
24
24
  import { materializeAgentHome } from '../agent-home.mjs';
25
25
  import { memorySnapshotSync } from '../hermes-cli.mjs';
26
26
  import { createRequire } from 'node:module';
@@ -684,28 +684,20 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
684
684
  // the model it runs is recorded with the session, for caps that count by model
685
685
  const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
686
686
  try {
687
- const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context),machine=normalMachine(task.machine)===localMachine()?null:task.machine;
688
- const cardOptions={root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
689
- profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null,machine,context};
690
- // Situation 16: with no reachable Teams the card's agent is its locally minted identity, declared later under the same id.
691
- let agent=null,previous=null,reader=null;
692
- if(!team.offline){
693
- try{
694
- agent=ensureCardAgent(cardOptions);
695
- previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
696
- reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
697
- }catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
698
- }
699
- agent??=localCardAgent(cardOptions);
687
+ // Teams is never on a launch's path: the card's agent is its identity minted on this host under its retained intent,
688
+ // and its sessions are joined to it off this path, after the launch. No Teams answer starts, stops or holds a card.
689
+ const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context);
690
+ const agent=localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
691
+ profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
700
692
  // G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
701
693
  const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
702
694
  const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
703
695
  if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
704
- previous??=localAgentSessions(board.root,team.org,agent.identity.id);
696
+ const previous=localAgentSessions(board.root,team.org,agent.identity.id);
705
697
  prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
706
698
  Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
707
699
  agent_profile_intent:structuredClone(agent.intended.resolved),
708
- agent_env:agentEnvironment(agent,{root:board.root,context:reader?.context})});
700
+ agent_env:agentEnvironment(agent,{root:board.root,context:normalMachine(task.machine??localMachine())===localMachine()?localReaderContext(team.org,agent.identity.id)??undefined:undefined})});
709
701
  // The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
710
702
  // another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
711
703
  const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
@@ -718,14 +710,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
718
710
  setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
719
711
  event(db,id,'restart_authorization_consumed',{},runId);
720
712
  });
721
- if(result?.session_id){
722
- const ref=`${result.harness}:${result.session_id}`;
723
- recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
724
- if(agent.synced!==false){
725
- try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
726
- catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
727
- }
728
- }
713
+ if(result?.session_id)recordAgentRun(board.root,team.org,agent.identity.id,{session:`${result.harness}:${result.session_id}`,machine:result.machine,run:runId});
729
714
  return result;
730
715
  } catch(error) {
731
716
  // An issued asynchronous launch may bind later. Its original permission
@@ -1831,7 +1816,7 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1831
1816
  const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
1832
1817
  const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
1833
1818
  const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
1834
- // The request is claimed before this adoption's first write outside the board (the agent's Teams record, below), in
1819
+ // The request is claimed before this adoption's first write outside the board (the agent store, below), in
1835
1820
  // a transaction that finds it still open and the card's run the one read: a request answered or claimed meanwhile, or
1836
1821
  // a run that changed, ends it here with nothing written anywhere, and no other round acts on a request claimed here.
1837
1822
  const stale = (db) => {
@@ -1845,11 +1830,8 @@ function adoptSession(board, id, who, { request = null, open = request, progress
1845
1830
  event(db, id, 'adoption_claimed', { request, session: who, ...newClaim(ttl) });
1846
1831
  });
1847
1832
  progress.claimed = request !== null;
1848
- let agent = null;
1849
- if (lane && !team.offline) {
1850
- try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
1851
- catch (error) { if (!teamsUnavailable(error)) throw error; }
1852
- }
1833
+ // The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
1834
+ const agent = lane ? localCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null }) : null;
1853
1835
  // A session another agent already owns is refused before anything is recorded: the run would name one agent and the
1854
1836
  // agent store another, and every round after would trip on it.
1855
1837
  const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
package/index.d.ts CHANGED
@@ -128,10 +128,10 @@ export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: str
128
128
  export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
129
129
  export function agentPreamble(agent: ReturnType<typeof ensureCardAgent>, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
130
130
  export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
131
- /** The Org for agent identity; `offline` when no Teams context is selected or reachable (situation 16). */
131
+ /** The Org for agent identity, read from this home's selected context without a request; `offline` when none is selected. Throws when a named context cannot be read. */
132
132
  export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
133
133
  export function teamsUnavailable(error: unknown): boolean;
134
- /** Locally minted identities (`identity.synced === false`), declared later under the same id. */
134
+ /** The identity a run starts as without asking Teams; `identity.synced` is false until Teams has its declaration, made later under the same id. */
135
135
  export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
136
136
  export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
137
137
  export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.36",
3
+ "version": "0.5.37",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {