@volter/supercode-orchestrator 0.5.41 → 0.5.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/activation.mjs CHANGED
@@ -7,6 +7,7 @@ import { homedir } from 'node:os';
7
7
  import { mkdirSync, readFileSync, readdirSync, lstatSync, rmSync, writeFileSync, existsSync } from 'node:fs';
8
8
  import { CLAUDE_BRIDGE, linkTo, skillSources } from './hermes-home.mjs';
9
9
  import { ownLogin } from './worker-route.mjs';
10
+ import { execFile } from 'node:child_process';
10
11
 
11
12
  /** Which env var each harness reads its config home from (supercode registry evidence). */
12
13
  export const HOME_ENV = Object.freeze({
@@ -54,6 +55,24 @@ export function activationEnv({ root, profile, surfaceKey, vault }) {
54
55
  return env;
55
56
  }
56
57
 
58
+ const declaredHomes = new Set();
59
+ /**
60
+ * A worker that keeps its sessions in a config home of its own (its profile's CODEX_HOME or CLAUDE_CONFIG_DIR) is
61
+ * declared to this machine's connector through its own door (`supercode teams roots home add`), on each root covering
62
+ * the folder it runs in, so its sessions are found and published (D138 step 5). Once per home and folder per process,
63
+ * off the launch's path: the door is local, and a refusal (no connected root covers the folder) is only said.
64
+ */
65
+ export function declareWorkerHome({ harness, env, cwd, log = () => {} }) {
66
+ const home = ['codex', 'claude-code'].includes(harness) ? env?.[HOME_ENV[harness]] : null;
67
+ if (!home || declaredHomes.has(`${harness}\0${home}\0${cwd}`)) return;
68
+ declaredHomes.add(`${harness}\0${home}\0${cwd}`);
69
+ execFile(process.env.SUPERCODE_BIN || 'supercode', ['teams', 'roots', 'home', 'add', home, '--harness', harness, '--cwd', cwd, '--json'], { timeout: 30_000 }, (error, _stdout, stderr) => {
70
+ if (!error) return;
71
+ declaredHomes.delete(`${harness}\0${home}\0${cwd}`);
72
+ log('warn', `worker home ${home} not declared to this machine's connector: ${String(stderr || error.message).trim().split('\n').at(-1)}`);
73
+ });
74
+ }
75
+
57
76
  export function activationCwd(profile, workdir) {
58
77
  return resolve(profile.dir, workdir ?? profile.worker?.cwd ?? '.');
59
78
  }
package/agents.mjs CHANGED
@@ -15,7 +15,8 @@ const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
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
16
  export function agentDoor(op,args=[],{context,timeout=25000}={}){
17
17
  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
- 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);}
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
20
  return JSON.parse(reply.stdout);
20
21
  }
21
22
  /** Teams is not configured, not reachable, or no longer accepts this login: identity stays local until it syncs.
@@ -31,13 +32,12 @@ function store(root){
31
32
  mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
32
33
  db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
33
34
  CREATE TABLE IF NOT EXISTS agent_intents(org TEXT NOT NULL,id TEXT NOT NULL,key TEXT NOT NULL,profile_ref TEXT NOT NULL,profile_json TEXT NOT NULL,declared INTEGER NOT NULL DEFAULT 0,declaration_json TEXT,created_at INTEGER NOT NULL,PRIMARY KEY(org,id),UNIQUE(org,key));
34
- CREATE TABLE IF NOT EXISTS agent_run_retries(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,next_at INTEGER NOT NULL,wait_ms INTEGER NOT NULL,PRIMARY KEY(org,agent_id,session_ref));
35
35
  CREATE TABLE IF NOT EXISTS agent_declaration_heads(org TEXT NOT NULL,key TEXT NOT NULL,agent_id TEXT NOT NULL,generation INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(org,key));
36
36
  CREATE TABLE IF NOT EXISTS agent_assignments(org TEXT NOT NULL,id TEXT NOT NULL,agent_id TEXT NOT NULL,kind TEXT NOT NULL,target TEXT NOT NULL,role TEXT NOT NULL,state TEXT NOT NULL,started_at INTEGER NOT NULL,ended_at INTEGER,PRIMARY KEY(org,id));
37
37
  CREATE INDEX IF NOT EXISTS agent_active_assignments ON agent_assignments(org,agent_id,state);
38
38
  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
39
  CREATE TABLE IF NOT EXISTS agent_runtime_sessions(org TEXT NOT NULL,runtime_ref TEXT NOT NULL,session_ref TEXT NOT NULL,PRIMARY KEY(org,runtime_ref));
40
- CREATE TABLE IF NOT EXISTS agent_runs(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,machine TEXT,run_id TEXT,profile_json TEXT,published INTEGER NOT NULL DEFAULT 0,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));`);
40
+ CREATE TABLE IF NOT EXISTS agent_runs(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,machine TEXT,run_id TEXT,profile_json TEXT,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));`);
41
41
  // A declaration Teams refused is asked again a bounded number of times (synchronizeLocalAgents), then left failed.
42
42
  const columns=new Set(db.prepare('PRAGMA table_info(agent_intents)').all().map(c=>c.name));
43
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}`);
@@ -91,7 +91,6 @@ export function synchronizeAgentEnds(root,org,{context}={}){
91
91
  try{
92
92
  for(const row of db.prepare('SELECT * FROM agent_end_outbox WHERE org=? ORDER BY created_at LIMIT 100').all(org)){
93
93
  if(db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(org,row.agent_id)){db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(org,row.agent_id);continue;}
94
- if(db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND published<=0').get(org,row.agent_id)){pending.push({agent:row.agent_id,error:'session ownership publication remains pending'});continue;}
95
94
  // claimed by compare-and-set on its claimed_at (free, or a claim older than SYNC_CLAIM_MS from a pass that died);
96
95
  // deleted only after Teams ended the agent (the end is idempotent by its digest), handed back when it failed
97
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;
@@ -399,42 +398,12 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
399
398
  const db=store(root);try{
400
399
  db.exec('BEGIN IMMEDIATE');
401
400
  db.prepare('INSERT INTO agent_runtime_sessions(org,runtime_ref,session_ref) VALUES (?,?,?) ON CONFLICT(org,runtime_ref) DO UPDATE SET session_ref=excluded.session_ref').run(owner.org,runtimeRef,session);
402
- // A published native session remains history; an unpublished lease-only row is not a session.
403
- db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=? AND published<=0').run(owner.org,runtimeRef);
401
+ // The lease-only row is not a session: its native session is the run now.
402
+ db.prepare('DELETE FROM agent_runs WHERE org=? AND session_ref=?').run(owner.org,runtimeRef);
404
403
  db.exec('COMMIT');
405
404
  }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
406
405
  return agentRunForSession(root,[runtimeRef]);
407
406
  }
408
- /** Join an observed native session through the public catalogue; unknown catalogue state remains pending. */
409
- // A run whose session ownership Teams refused (a 4xx answer to the claim: not found, forbidden, conflict, signed out)
410
- // is asked again after a growing wait (two minutes, doubling to an hour), not every round: each ask spends the shared
411
- // credential's allowance. Anything else (the rate limit, a timeout, a 5xx, the local daemon's bind of a session just
412
- // spawned) is not the run's own and is asked again next round.
413
- // The wait is kept in this home's agent store, not in the asking process: a one-shot `workflow dispatch` asked again
414
- // on every run when each process kept its own, and every ask spends the shared credential's allowance.
415
- const retryOf=(root,org,row)=>{const db=store(root);try{return db.prepare('SELECT next_at,wait_ms FROM agent_run_retries WHERE org=? AND agent_id=? AND session_ref=?').get(org,row.agent_id,row.session_ref)??null;}finally{db.close();}};
416
- const refused=error=>error?.status>=400&&error.status<500&&error.status!==408&&error.status!==429;
417
- export function synchronizeAgentRun(root,org,row,{context,intended}={}) {
418
- let answer;
419
- try{answer=joinAgentRun(root,org,row,{context,intended});}
420
- catch(error){
421
- if(error?.claimRefused){
422
- const wait=Math.min((retryOf(root,org,row)?.wait_ms??60000)*2,3600000),db=store(root);
423
- try{db.prepare('INSERT INTO agent_run_retries(org,agent_id,session_ref,next_at,wait_ms) VALUES (?,?,?,?,?) ON CONFLICT(org,agent_id,session_ref) DO UPDATE SET next_at=excluded.next_at,wait_ms=excluded.wait_ms').run(org,row.agent_id,row.session_ref,Date.now()+wait,wait);}finally{db.close();}
424
- }
425
- throw error;
426
- }
427
- const db=store(root);try{db.prepare('DELETE FROM agent_run_retries WHERE org=? AND agent_id=? AND session_ref=?').run(org,row.agent_id,row.session_ref);}finally{db.close();}
428
- return answer;
429
- }
430
- function joinAgentRun(root,org,row,{context,intended}) {
431
- const claim=(...args)=>{try{return agentDoor(...args);}catch(error){throw Object.assign(error,{claimRefused:refused(error)});}};
432
- const response=claim('ownSession',[row.agent_id,{session:row.session_ref,main:row.main!==0&&row.main!==false,...(row.starter_session?{starter_session:row.starter_session}:{})},digest(`${org}/${row.agent_id}/${row.session_ref}`)],{context});
433
- const intent=agentIntent(root,org,row.agent_id);
434
- const runtime=agentDoor('bindContext',[row.agent_id,row.session_ref,row.machine,intent?.assignments??[],intended??intent?.profile.resolved],{context});
435
- publishAgentRun(root,org,row.session_ref);
436
- return {...response.data,runtime};
437
- }
438
407
  export function recordAgentRun(root,org,id,{session,machine=null,run,profile=null,main=true,starterSession=null}){
439
408
  const db=store(root);try{
440
409
  db.exec('BEGIN IMMEDIATE');
@@ -444,13 +413,6 @@ export function recordAgentRun(root,org,id,{session,machine=null,run,profile=nul
444
413
  db.exec('COMMIT');
445
414
  }catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
446
415
  }
447
- // A run waiting to be joined has published=0; a pass joining it holds published=-(the time it claimed it), by
448
- // compare-and-set, and a claim older than SYNC_CLAIM_MS (a pass that died) is free again.
449
- export function pendingAgentRuns(root,org){const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND (published=0 OR (published<0 AND -published<?)) ORDER BY rowid LIMIT 100').all(org,Date.now()-SYNC_CLAIM_MS).filter(row=>!(retryOf(root,org,row)?.next_at>Date.now()));}finally{db.close();}}
450
- /** Claim one waiting run for this pass; false when another pass holds it. */
451
- export function claimAgentRun(root,org,row){const db=store(root);try{return db.prepare('UPDATE agent_runs SET published=? WHERE org=? AND session_ref=? AND published=?').run(-Date.now(),org,row.session_ref,row.published).changes===1;}finally{db.close();}}
452
- /** Give a claimed run back, unjoined. */
453
- export function releaseAgentRun(root,org,row){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=0 WHERE org=? AND session_ref=? AND published<0').run(org,row.session_ref);}finally{db.close();}}
454
416
  /** Session references come from the bound runtime, never from a model's requested agent id. */
455
417
  export function agentRunForSession(root, references) {
456
418
  const db = store(root);
@@ -496,9 +458,6 @@ export function runBudgetRefusal(root,agent,budgets,now){
496
458
  }
497
459
  return null;
498
460
  }
499
- /** A run whose session Teams never had and that no longer runs here can never be owned there: it leaves the sync. */
500
- export function abandonAgentRun(root,org,session){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=2 WHERE org=? AND session_ref=? AND published<=0').run(org,session);db.prepare('DELETE FROM agent_run_retries WHERE org=? AND session_ref=?').run(org,session);}finally{db.close();}}
501
- export function publishAgentRun(root,org,session){const db=store(root);try{db.prepare('UPDATE agent_runs SET published=1 WHERE org=? AND session_ref=?').run(org,session);}finally{db.close();}}
502
461
  export function agentPreamble(agent,{main=true,previous=[]}={}){
503
462
  const {identity:a,intended,assignments=[]}=agent;
504
463
  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'):''}`;
@@ -32,23 +32,6 @@ try{
32
32
  result=agent.state==='ended'?{data:{id:agent.id,state:'ended'}}:await team.agents.end(agent.id,{reason:args[1]},{...options,ifMatch:agent.revision,idempotencyKey:args[2]});
33
33
  }
34
34
  else if(op==='reader')result=await team.agents.delegateReader(args[0],{},{...options,ifMatch:args[1]});
35
- else if(op==='bindContext'){
36
- // The binding names the agent's reader context on the session's machine, so it installs that context itself: a
37
- // launch installs one before its session starts, but an adopted session never had one, and a context installed a
38
- // day before has expired. An ended agent has neither a reader nor a live session: Teams revoked its credentials
39
- // and ended its sessions when it ended, so there is nothing to bind.
40
- const agent=(await team.agents.get(args[0],options)).data;
41
- if(agent.state==='ended')result={bound:false,reason:`agent ${agent.id} ended`};
42
- else{
43
- const reader=await team.agents.delegateReader(agent.id,{expires_in_ms:86400000},{...options,ifMatch:agent.revision});
44
- const channel=await openMachine({machine:reach(args[2]),context:context??process.env.SUPERCODE_CONTEXT});
45
- try{
46
- const rpc=new StreamRpc(await channel.open('node'));
47
- const installed=await rpc.call('harness.v1.agents.context',{origin:scope.origin,team:scope.team_id,agent:agent.id,credential:reader.data});
48
- result=await rpc.call('harness.v1.agents.bindSession',{session:args[1],context:installed.context,assignments:args[3]??[],intended:args[4]??null});
49
- }finally{channel.close();}
50
- }
51
- }
52
35
  else if(op==='readerContext'){
53
36
  const identity=await team.agents.get(args[0],options);
54
37
  const reader=await team.agents.delegateReader(args[0],{expires_in_ms:86400000},{...options,ifMatch:identity.data.revision});
package/board/cli.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  // which lands here. The home is `--root`, else `SUPERCODE_ORCHESTRATOR_HOME`, else `HERMES_HOME`; the board is
6
6
  // `--board <slug>`, else `HERMES_KANBAN_BOARD`, else the home's current board (`kanban/current`), else `default`.
7
7
  import { taskStatusOf } from './published.mjs';
8
- import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
8
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { recordSlow, takeSteps } from '@volter/supercode-harness-sdk/slow-log';
11
11
  import { basename, join, resolve } from 'node:path';
@@ -15,6 +15,19 @@ import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath,
15
15
  import { isLocalWorkspace } from './workspace.mjs';
16
16
  import { openZtrackBoard } from './ztrack.mjs';
17
17
  import * as engine from './engine.mjs';
18
+ import { runtimeDoor } from './runtime-door.mjs';
19
+
20
+ /**
21
+ * What this machine's connector reports of publishing this board home (`harness.v1.boards.list`): whether the team
22
+ * refused its credential, the publication it is retrying and the ones it abandoned. Null when no daemon answers or the
23
+ * home is not registered: diagnostics then say nothing of publishing.
24
+ */
25
+ function boardPublishing(root) {
26
+ const read = runtimeDoor(null, 'harness.v1.boards.list', {}, { timeout: 15_000 });
27
+ if (read.code !== 0) return null;
28
+ let here; try { here = realpathSync(root); } catch { here = resolve(root); }
29
+ try { const listed = JSON.parse(read.stdout); const board = listed.boards?.find((b) => b.root === here); return board ? { ...board, refused: listed.refused ?? null } : null; } catch { return null; }
30
+ }
18
31
  import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick, deliver, startDelivery } from './dispatch.mjs';
19
32
  import { watchBoard } from './wake.mjs';
20
33
  import { streamBoardPublications } from './event-stream.mjs';
@@ -819,10 +832,12 @@ export async function runWorkflow(argv) {
819
832
  case 'diagnostics': case 'diag': {
820
833
  const { settings } = await laneState(root).catch(() => ({ settings: boardSettings({}) }));
821
834
  const fleet = observeFleet(root);
835
+ const publishing = boardPublishing(root);
822
836
  const order = ['warning', 'error', 'critical'];
823
837
  const found = withBoard((b) => {
824
838
  const cards = args.task ? [engine.getTask(b.db, args.task)].filter(Boolean) : engine.listCards(b.db, {});
825
- return cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))
839
+ return [...(args.task ? [] : engine.diagnosePublishing(publishing).map((d) => ({ task: '(board)', title_of_task: null, ...d }))),
840
+ ...cards.flatMap((t) => engine.diagnose(b.db, t, { failureLimit: settings.failureLimit, fleet, launchWindowSeconds: settings.launchWindow, publishing }).map((d) => ({ task: t.id, title_of_task: t.title, ...d })))]
826
841
  .filter((d) => !args.severity || order.indexOf(d.severity) >= order.indexOf(args.severity));
827
842
  });
828
843
  if (args.json) { say(args, '', found); return 0; }
@@ -1448,6 +1463,13 @@ async function serve(root, args = {}) {
1448
1463
  const running = installed();
1449
1464
  log(`dispatching the boards of ${root} from ${localMachine()} (orchestrator ${running ?? 'unknown'})`);
1450
1465
  events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log });
1466
+ // The board's events are published by this machine's connector, not by the dispatcher (D137, D138 step 4): the home is
1467
+ // named to the daemon once, and the connector follows its `workflow events` from then on, with or without this serve.
1468
+ { const named = runtimeDoor(null, 'harness.v1.boards.register', { root }, { timeout: 30_000 });
1469
+ const refused = named.code === 0 ? JSON.parse(named.stdout || '{}').refused ?? null : null;
1470
+ log(named.code !== 0 ? `board events: not published; the machine daemon did not take this home: ${(named.stderr || named.stdout).trim().split('\n').at(-1)}`
1471
+ : refused ? `board events: registered, but the team refused this machine's credential (${refused.status}); nothing is published until it is re-enrolled`
1472
+ : `board events: this home is published by the machine's connector`); }
1451
1473
  log('dispatch wakes on board writes, document edits and board mail; the interval sweep reads each machine\'s own door');
1452
1474
  const stuck = {};
1453
1475
  const warned = {};
@@ -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, pendingAgentRuns, synchronizeAgentRun, claimAgentRun, releaseAgentRun, abandonAgentRun,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
23
+ import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, 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';
@@ -160,11 +160,6 @@ const sessionAlive = (fleet, session) => fleet.native?.has(addressOf(session)) ?
160
160
  const sessionAt = (address) => { const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(address)); return m && HARNESSES[m[2]] ? { address, machine: normalMachine(m[1]), harness: m[2], session_id: m[3] } : null; };
161
161
  const subscriberAlive = (fleet, address) => fleet.alive.has(address) || (sessionAt(address) ? sessionAlive(fleet, sessionAt(address)) === true : false);
162
162
  const paneLists = new Map();
163
- // A run the agent sync holds names only its machine and native session (`harness:id`), never a pane or pid.
164
- function syncRowAlive(fleet, row) {
165
- const machine = row.machine || localMachine(), at = row.session_ref.indexOf(':');
166
- return sessionAlive(fleet, { machine, harness: row.session_ref.slice(0, at), session_id: row.session_ref.slice(at + 1), address: `sc:${machine}:${row.session_ref}` });
167
- }
168
163
  function unpublishedAlive(session) {
169
164
  if (!session) return null;
170
165
  if (session.surface === 'headless' && session.pid) return pidAlive(session.pid);
@@ -1307,9 +1302,10 @@ export function reconcileCallerLaunch(board, sid) {
1307
1302
  }
1308
1303
  }
1309
1304
 
1310
- /** The board's agent records joined to Teams: declarations, ends, session ownership and usage. Teams I/O, so it runs in
1311
- * the delivery pass (`deliver`), never in a dispatch round. */
1312
- function synchronizeAgents(board, fleet, report, entry) {
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) {
1313
1309
  // A delayed launch receipt/catalogue event binds the same declared agent, never another identity.
1314
1310
  // A resumed session is held by its newest run; an earlier run of the same session no longer owns it.
1315
1311
  const agentRuns=[...new Map(view(board,db=>db.prepare("SELECT id,task_id,metadata,ended_at FROM task_runs WHERE json_extract(metadata,'$.supercode.agent_id') IS NOT NULL ORDER BY id").all())
@@ -1319,13 +1315,13 @@ function synchronizeAgents(board, fleet, report, entry) {
1319
1315
  const session=json(row.metadata,{})?.supercode;
1320
1316
  if(!session?.session_id||!session.agent_org)continue;
1321
1317
  const ref=`${session.harness}:${session.session_id}`;
1322
- // An ended run whose session the agent store already holds, published, for another agent lost that claim when it
1323
- // ran: no round can change an ended run or a published owner, so the disagreement is history. It is said once on
1324
- // its card (agent_ownership_conflict) and the run is not recorded again. A live run is never history: it is
1325
- // recorded below every round, and a conflict there is refused and said each round as before.
1318
+ // An ended run whose session the agent store already holds for another agent lost that claim when it ran: no round
1319
+ // can change an ended run, so the disagreement is history. It is said once on its card (agent_ownership_conflict)
1320
+ // and the run is not recorded again. A live run is never history: it is recorded below every round, and a conflict
1321
+ // there is refused and said each round as before.
1326
1322
  if(row.ended_at){
1327
1323
  let owner=null;try{owner=agentRunForSession(board.root,[ref]);}catch{/* conflicting owners: recorded below, refused there */}
1328
- if(owner?.published===1&&owner.org===session.agent_org&&owner.agent_id!==session.agent_id){
1324
+ if(owner&&owner.org===session.agent_org&&owner.agent_id!==session.agent_id){
1329
1325
  const said=db=>db.prepare("SELECT 1 FROM task_events WHERE task_id=? AND run_id=? AND kind='agent_ownership_conflict' LIMIT 1").get(row.task_id,row.id);
1330
1326
  if(!view(board,said))tx(board,db=>{if(!said(db))event(db,row.task_id,'agent_ownership_conflict',{session:ref,run_agent:session.agent_id,owner:owner.agent_id,owner_run:owner.run_id},row.id);});
1331
1327
  continue;
@@ -1366,13 +1362,6 @@ function synchronizeAgents(board, fleet, report, entry) {
1366
1362
  if(task&&(task.archived||['done','cancelled'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
1367
1363
  }
1368
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}`]);
1369
- for(const [org,context] of orgs)for(const row of pendingAgentRuns(board.root,org)){
1370
- const launch=agentRuns.find(run=>String(run.id)===String(row.run_id));
1371
- // each run is claimed by compare-and-set on its own `published` before it is joined; another pass's claim skips it
1372
- if(!claimAgentRun(board.root,org,row))continue;
1373
- try{synchronizeAgentRun(board.root,org,row,{context,intended:json(launch?.metadata,{})?.supercode?.agent_profile_intent});}
1374
- catch(error){releaseAgentRun(board.root,org,row);if(error.code==='rate_limited'){report.queued.push([row.run_id,`agent session sync pending: ${error.message}`]);break;}const gone=error.claimRefused&&error.status===404&&fleet.ok&&syncRowAlive(fleet,row)===false;if(gone)abandonAgentRun(board.root,org,row.session_ref);report.failed.push([row.run_id,`agent session sync failed: ${error.message}${gone?`; ${row.session_ref} is stopped and Teams refuses it, so it leaves the sync`:''}`]);}
1375
- }
1376
1365
  }
1377
1366
 
1378
1367
  function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes) {
@@ -2668,7 +2657,7 @@ export async function deliver({ root, slugs = null }) {
2668
2657
  tx(board, (db) => queueBoardMail(db)); phase('mail');
2669
2658
  report.effects = await runEffects(board, [], { pending: true }); phase('effects');
2670
2659
  await notify(board, fleet, report, entry); phase('notify');
2671
- synchronizeAgents(board, fleet, report, entry); phase('agents');
2660
+ synchronizeAgents(board, report, entry); phase('agents');
2672
2661
  report.effects_failed = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NOT NULL').get().n);
2673
2662
  report.effects_waiting = Number(board.db.prepare('SELECT COUNT(*) AS n FROM supercode_effects WHERE failed_at IS NULL').get().n);
2674
2663
  } catch (error) { report.error = error.message; }
package/board/engine.mjs CHANGED
@@ -912,12 +912,20 @@ export function gcEvents(board, seconds) {
912
912
 
913
913
  const hours = (s) => s / 3600;
914
914
 
915
+ /** The board home's publishing, as its machine's connector reports it (`harness.v1.boards.list`): what holds it all. */
916
+ export function diagnosePublishing(publishing) {
917
+ if (!publishing) return [];
918
+ if (publishing.refused) return [{ kind: 'publishing_refused', severity: 'error', title: `the team refused this machine's credential (${publishing.refused.status}); no card of this home is published until the machine is re-enrolled`, data: publishing.refused, actions: ['supercode teams enroll'] }];
919
+ if (publishing.error) return [{ kind: 'publishing_waits', severity: 'warning', title: `publishing to the team waits and is retried: ${publishing.error}`, data: { failing: publishing.failing ?? null }, actions: [] }];
920
+ return [];
921
+ }
922
+
915
923
  /**
916
924
  * Distress signals on a card, computed on demand and never stored (Hermes's diagnostics, the same kinds where the
917
925
  * same thing can go wrong, and supercode's own where only its sessions can): each `{ kind, severity, title, data,
918
926
  * actions }`, worst first.
919
927
  */
920
- export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300 } = {}) {
928
+ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, strandedSeconds = 30 * 60, blockedHours = 24, fleet = null, launchWindowSeconds = 300, publishing = null } = {}) {
921
929
  const out = [];
922
930
  const t = now();
923
931
  const events = all(db, 'SELECT kind, payload, created_at FROM task_events WHERE task_id = ? ORDER BY id', task.id);
@@ -979,6 +987,10 @@ export function diagnose(db, task, { failureLimit = DEFAULT_FAILURE_LIMIT, stran
979
987
  const p = json(row.payload, {}) ?? {};
980
988
  out.push({ kind: 'notice_abandoned', severity: 'warning', title: `the notice of event ${p.source_event} to ${p.address} was abandoned after ${p.attempts} attempts: ${p.error ?? 'no reason recorded'}`, data: { event: p.source_event, address: p.address, attempts: p.attempts }, actions: [] });
981
989
  }
990
+ // publications of the card the machine's connector abandoned: the team refused each one past its attempts
991
+ for (const a of (publishing?.abandoned ?? []).filter((a) => a.card === task.id).slice(0, 20)) {
992
+ out.push({ kind: 'publication_abandoned', severity: 'warning', title: `its ${a.kind} (sequence ${a.sequence}) was not published to the team: refused ${a.attempts} times (${a.status} ${a.code ?? ''}): ${a.error}`, data: { publication: a.publication, sequence: a.sequence, status: a.status, code: a.code }, actions: [] });
993
+ }
982
994
  const order = ['critical', 'error', 'warning'];
983
995
  return out.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity));
984
996
  }
@@ -81,9 +81,11 @@ export async function* followBoardPublications({root,after,signal}) {
81
81
  // The transaction's watermark fences concurrent mutations. Replay
82
82
  // after it repairs any head updated while this inventory is read.
83
83
  for(const sequence of boundary.heads){
84
- const row=board.db.prepare('SELECT snapshot FROM supercode_publications WHERE sequence=?').get(sequence);
84
+ const row=board.db.prepare('SELECT id,kind,sequence,snapshot FROM supercode_publications WHERE sequence=?').get(sequence);
85
85
  if(!row)throw resync('Workflow snapshot unavailable');
86
- yield {type:'snapshot_item',data:{board:slug,card:JSON.parse(row.snapshot)}};
86
+ // each item names the publication it is (its id, kind and sequence), as an event does, so a follower that
87
+ // republishes it keys and orders it the same way however it first read it
88
+ yield {type:'snapshot_item',id:row.id,kind:row.kind,sequence:row.sequence,data:{board:slug,card:JSON.parse(row.snapshot)}};
87
89
  }
88
90
  positions[slug]={log:boundary.log,sequence:boundary.high};
89
91
  }else{
@@ -91,7 +93,7 @@ export async function* followBoardPublications({root,after,signal}) {
91
93
  if(!position||position.log!==boundary.log||!Number.isSafeInteger(position.sequence)||position.sequence<boundary.floor||position.sequence>boundary.high)throw resync(`Workflow publication log changed: ${slug}`);
92
94
  for(;;){
93
95
  const rows=board.db.prepare('SELECT * FROM supercode_publications WHERE sequence>? ORDER BY sequence LIMIT 32').all(positions[slug].sequence);if(!rows.length)break;
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)}};}
96
+ for(const row of rows){positions[slug].sequence=row.sequence;yield {type:'event',id:row.id,kind:row.kind,sequence:row.sequence,cursor:cursorOf(home,positions),data:{board:slug,card:JSON.parse(row.snapshot)}};}
95
97
  }
96
98
  }
97
99
  }finally{board.close();const paths=backingPaths(root,slug);for(const path of [paths.sqlite,`${paths.sqlite}-wal`])settled.set(path,statOf(path));}
@@ -1,10 +1,6 @@
1
1
  // The card writer commits the full snapshot and a retained delivery obligation together.
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { realpathSync } from 'node:fs';
4
3
  export const PUBLICATION_SCHEMA = `
5
- CREATE TABLE IF NOT EXISTS supercode_publication_receipts (
6
- event_id TEXT NOT NULL, destination TEXT NOT NULL, PRIMARY KEY(event_id,destination)
7
- );
8
4
  CREATE TABLE IF NOT EXISTS supercode_publications (
9
5
  sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, resource_id TEXT NOT NULL,
10
6
  kind TEXT NOT NULL, snapshot TEXT NOT NULL, actor TEXT, created_at INTEGER NOT NULL
@@ -103,39 +99,8 @@ export function recordCardPublications(db,actor) {
103
99
  if(history>2*RETAINED_CHANGES){
104
100
  const cut=Number(db.prepare('SELECT sequence FROM supercode_publications WHERE sequence NOT IN (SELECT sequence FROM supercode_publication_heads) ORDER BY sequence DESC LIMIT 1 OFFSET ?').get(RETAINED_CHANGES).sequence);
105
101
  const old='SELECT id FROM supercode_publications WHERE sequence<=? AND sequence NOT IN (SELECT sequence FROM supercode_publication_heads)';
106
- db.prepare(`DELETE FROM supercode_publication_receipts WHERE event_id IN (${old})`).run(cut);
107
102
  db.prepare(`DELETE FROM supercode_publications WHERE id IN (${old})`).run(cut);
108
103
  db.prepare('UPDATE supercode_publication_log SET pruned_through=MAX(pruned_through,?)').run(cut);
109
104
  }
110
105
  return changed;
111
106
  }
112
- export const boardSourceName = (root,slug) => `board:${hash(realpathSync(root)).slice(0,24)}:${slug}`;
113
-
114
- /**
115
- * The card as one published event may carry it (`limit`: the bytes its `data`, `{ board, card }`, may serialize to).
116
- * The board keeps every card whole; a card too large to send keeps its newest history: the oldest comments, attempts
117
- * and reviews go first, then the body's tail, and `omitted` counts what the event leaves out.
118
- */
119
- export function publishableCard(card,board,limit) {
120
- const bytes=value=>Buffer.byteLength(JSON.stringify(value));
121
- const size=value=>bytes({board,card:value});
122
- if(size(card)<=limit)return card;
123
- const omitted={comments:0,attempts:0,reviews:0,body_chars:0};
124
- const fitted={...card,omitted};
125
- // Counted once per item (its bytes and a separator), then checked whole: a card of megabytes fits in one pass.
126
- let over=size(fitted)-limit;
127
- for(const field of ['comments','attempts','reviews']){
128
- const items=card[field]??[];let drop=0;
129
- while(drop<items.length&&over>0){over-=bytes(items[drop])+1;drop+=1;}
130
- fitted[field]=items.slice(drop);omitted[field]=drop;
131
- }
132
- for(const field of ['comments','attempts','reviews']){
133
- while(fitted[field].length&&size(fitted)>limit){fitted[field]=fitted[field].slice(1);omitted[field]+=1;}
134
- }
135
- const body=String(card.body??'');
136
- for(let keep=body.length;size(fitted)>limit&&keep>0;){
137
- keep=Math.max(0,Math.min(keep-1,keep-(size(fitted)-limit)-64));
138
- fitted.body=body.slice(0,keep);omitted.body_chars=body.length-keep;
139
- }
140
- return fitted;
141
- }
package/board/wake.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  import { watch, existsSync, readFileSync, readdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
4
4
  import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { randomUUID } from 'node:crypto';
6
- import { boardEventRelay } from './event-relay.mjs';
7
6
  import { fileURLToPath } from 'node:url';
8
7
 
9
8
  export function signalBoardWrite(root, actor) {
@@ -20,13 +19,11 @@ export function signalBoardWrite(root, actor) {
20
19
  export function watchBoard({ root, binary, env, log = () => {} }) {
21
20
  const watchers = new Map();
22
21
  let pending = null, waiter = null, closed = false;
23
- let relay = null;
24
22
  const wake = (source, at = Date.now()) => {
25
23
  if (closed) return;
26
24
  if (!pending) pending = { at, sources: new Set() };
27
25
  pending.at = Math.min(pending.at, at);
28
26
  pending.sources.add(source);
29
- void relay?.wake();
30
27
  waiter?.();
31
28
  };
32
29
  const directory = (path, key, accept) => {
@@ -91,9 +88,6 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
91
88
  }
92
89
  };
93
90
  refresh();
94
- const supercodeHome = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME,'supercode') : join(env.HOME ?? '', '.config','supercode'));
95
- relay = boardEventRelay({root,supercodeHome,log});
96
- void relay.wake();
97
91
  return {
98
92
  refresh,
99
93
  wake,
@@ -109,7 +103,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
109
103
  return result ? { at: result.at, sources: [...result.sources] } : null;
110
104
  },
111
105
  close() {
112
- closed = true; waiter?.(); relay.close();
106
+ closed = true; waiter?.();
113
107
  for (const handle of watchers.values()) handle.close();
114
108
  watchers.clear();
115
109
  },
package/board.mjs CHANGED
@@ -15,6 +15,7 @@ import { fileURLToPath } from 'node:url';
15
15
  import { summary } from './board/cli.mjs';
16
16
  import { commandEnv, notifyChats, startDelivery, supercodeBin } from './board/dispatch.mjs';
17
17
  import { watchBoard } from './board/wake.mjs';
18
+ import { runtimeDoor } from './board/runtime-door.mjs';
18
19
  import { workflowEntry } from './board/workflow.mjs';
19
20
  import { evaluate, holds } from './board/expr.mjs';
20
21
 
@@ -101,6 +102,9 @@ export async function startBoard({ root, orchestration = null, log = () => {}, a
101
102
  }
102
103
  };
103
104
  const events = watchBoard({ root, binary: supercodeBin(), env: commandEnv(), log: message => log('warn', message) });
105
+ // The board's events are published by this machine's connector (D137, D138 step 4): the home is named to it once.
106
+ { const named = runtimeDoor(null, 'harness.v1.boards.register', { root }, { timeout: 30_000 });
107
+ log(named.code === 0 ? 'info' : 'warn', named.code === 0 ? 'board: its events are published by the machine\'s connector' : `board: events not published; the machine daemon did not take this home: ${(named.stderr || named.stdout).trim().split('\n').at(-1)}`); }
104
108
  void (async () => {
105
109
  let wakeReason = null;
106
110
  while (!stopped) {
package/index.d.ts CHANGED
@@ -122,9 +122,6 @@ export function activeAgentAssignments(root: Path): Array<Record<string, unknown
122
122
  export function finishAgentAssignment(root: Path, org: string, assignment: string, reason: string): void;
123
123
  export function synchronizeAgentEnds(root: Path, org: string, options?: {context?: string}): Array<{agent: string; error: string}>;
124
124
  export function recordAgentRun(root: Path, org: string, id: string, run: {session: string; machine?: string | null; run: string | number; profile?: unknown; main?: boolean; starterSession?: string | null}): void;
125
- export function pendingAgentRuns(root: Path, org: string): Array<Record<string, unknown>>;
126
- export function abandonAgentRun(root: Path, org: string, session: string): void;
127
- export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: string; session_ref: string; machine?: string | null; main?: boolean | number; starter_session?: string | null}, options?: {context?: string; intended?: unknown}): import('@volter/teams/http-client').SessionIdentity & {runtime: unknown};
128
125
  export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
129
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;
130
127
  export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
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,pendingAgentRuns, synchronizeAgentRun, agentRunForSession,agentPreamble, agentEnvironment, agentProfileMismatch } from "./agents.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";
43
43
  export { agentStateHome,materializeAgentHome } from './agent-home.mjs';
package/kanban-worker.mjs CHANGED
@@ -12,7 +12,7 @@ import { existsSync, statSync } from 'node:fs';
12
12
  import { basename, dirname, join, resolve } from 'node:path';
13
13
  import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
14
14
  import { load } from './orchestration.mjs';
15
- import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
15
+ import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, declareWorkerHome, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
16
16
  import { workerRoute } from './worker-route.mjs';
17
17
  import { KANBAN_ENV, untrustedServerVerdict, hermesApproval, hermesInstall, memorySnapshot } from './hermes-cli.mjs';
18
18
  import { pinnedSkills, taskProtocol } from './board/worker-prompt.mjs';
@@ -94,6 +94,7 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
94
94
  arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...sandboxArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd }),
95
95
  env: { ...(base.env ?? {}), ...runEnv },
96
96
  };
97
+ declareWorkerHome({ harness, env: launch.env, cwd, log: (_level, line) => say(`hermes-worker: ${line}`) });
97
98
  const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
98
99
  const policy = approvalPolicy(profile, harness);
99
100
  const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
package/loop.mjs CHANGED
@@ -9,7 +9,7 @@ import { basename, join, dirname } from 'node:path';
9
9
  import { step, leaseId } from './reducer.mjs';
10
10
  import * as events from './events.mjs';
11
11
  import { read, save, retainWorld, releaseWorld } from './orchestration.mjs';
12
- import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, approvalPolicy, sandboxArguments, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
12
+ import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, declareWorkerHome, approvalPolicy, sandboxArguments, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
13
13
  import { workerRoute } from './worker-route.mjs';
14
14
  import { hermesAccess, hermesApproval, untrustedServerVerdict, hermesToolEnv, memorySnapshot, mirrorSession, mirrorState, recordIncident, sealMirror } from './hermes-cli.mjs';
15
15
  import { hermesSessionKey, homeChannel } from './ir.mjs';
@@ -24,7 +24,7 @@ import { harnessHomes } from './mcp/verbs.mjs';
24
24
  import { checkMonitor, runJobScript } from './gates.mjs';
25
25
  import { startBoard } from './board.mjs';
26
26
  import { SenderIdentityRecord } from '@volter/teams/sender-identity';
27
- import { agentDoor,agentScope,teamsUnavailable,ensureProfileAgent,localProfileAgent,localAgentRecord,localAgentSessions,synchronizeLocalAgents,ensureForkAgent,agentRunForKey,agentIntent,agentPreamble,agentEnvironment,recordAgentRun,recordNativeAgentSession,pendingAgentRuns,claimAgentRun,releaseAgentRun,synchronizeAgentRun,agentRunForSession } from './agents.mjs';
27
+ import { agentDoor,agentScope,teamsUnavailable,ensureProfileAgent,localProfileAgent,localAgentRecord,localAgentSessions,synchronizeLocalAgents,ensureForkAgent,agentRunForKey,agentIntent,agentPreamble,agentEnvironment,recordAgentRun,recordNativeAgentSession,agentRunForSession } from './agents.mjs';
28
28
  import { materializeAgentHome } from './agent-home.mjs';
29
29
  import { localMachine } from './board/dispatch.mjs';
30
30
  import { execFile } from 'node:child_process';
@@ -189,23 +189,19 @@ export class Orchestrator {
189
189
  tick(at = this.clock()) {
190
190
  this.queue = this.queue.then(() => this.adoptOutsideJobs()).catch((error) => this.log('warn', `cron/jobs.json not read back: ${error.message}`));
191
191
  return this.dispatch(events.tick(at, cronInactivitySeconds(), cronMaxParallel())).then(
192
- (result) => { this.tickerHeartbeat(true); this.synchronizeProfileSessions();return result; },
192
+ (result) => { this.tickerHeartbeat(true); this.declareLocalAgents();return result; },
193
193
  (error) => { this.tickerHeartbeat(false); throw error; },
194
194
  );
195
195
  }
196
196
 
197
- synchronizeProfileSessions(){
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. */
199
+ declareLocalAgents(){
198
200
  if(!existsSync(join(this.state.root,'agent-state.sqlite')))return;
199
201
  try{
200
- const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context);
201
- if(scope.offline)return;
202
+ const context=process.env.SUPERCODE_CONTEXT;
202
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}`);
203
- for(const row of pendingAgentRuns(this.state.root,scope.org)){
204
- if(!claimAgentRun(this.state.root,scope.org,row))continue;
205
- try{synchronizeAgentRun(this.state.root,scope.org,row,{context,intended:row.profile_json?JSON.parse(row.profile_json):undefined});}
206
- catch(error){releaseAgentRun(this.state.root,scope.org,row);this.log('warn',`agent session ${row.session_ref}: ${error.message}`);if(error.code==='rate_limited')break;}
207
- }
208
- }catch(error){this.log('warn',`agent session synchronization: ${error.message}`);}
204
+ }catch(error){this.log('warn',`agent declaration: ${error.message}`);}
209
205
  }
210
206
 
211
207
  /**
@@ -474,6 +470,7 @@ export class Orchestrator {
474
470
  const tools = worker.harness === 'hermes' ? {} : hermesToolEnv(agentHome);
475
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}) };
476
472
  env.HERMES_HOME=agentHome;
473
+ declareWorkerHome({ harness: worker.harness, env, cwd, log: (level, message) => this.log(level, message) });
477
474
  // All sessions of this agent carry its state; a shared profile contributes configuration only.
478
475
  const memory = INSTRUCTION_ARGUMENTS[worker.harness] ? await memorySnapshot(agentHome).catch((error) => { this.log('warn', `memory ${keyString}: ${error.message}`); return ''; }) : '';
479
476
  // a worker in the user's own home carries its persona there too (its config home's CLAUDE.md is the user's)
@@ -522,8 +519,6 @@ export class Orchestrator {
522
519
  if(!INSTRUCTION_ARGUMENTS[worker.harness])(this.runtimeAgentPreambles??=new Map()).set(lid,agentPrompt);
523
520
  const session=`${worker.harness}:${runtime.handle.runtime_id}`;
524
521
  recordAgentRun(this.state.root,scope.org,agent.identity.id,{session,machine:null,run:run_key??lid,profile:agent.intended.resolved,main,starterSession:starter_session});
525
- if(agent.synced!==false&&claimAgentRun(this.state.root,scope.org,{session_ref:session,published:0}))try{synchronizeAgentRun(this.state.root,scope.org,{agent_id:agent.identity.id,session_ref:session,machine:null,main},{context,intended:agent.intended.resolved});}
526
- catch(error){releaseAgentRun(this.state.root,scope.org,{session_ref:session});this.log('warn',`agent session ${session} retained for synchronization: ${error.message}`);}
527
522
  this.enqueue(events.worker({
528
523
  profile: profileName, keyString, runtime_id: runtime.handle.runtime_id,
529
524
  ev: { kind: 'started', runtime_id: runtime.handle.runtime_id, session_id: runtime.handle.runtime_id, connection: runtime.connection, harness: worker.harness, steer },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.41",
3
+ "version": "0.5.43",
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.13",
28
+ "@volter/teams": "0.2.15",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },
@@ -1,51 +0,0 @@
1
- // App-owned source names/maps live here; the Teams core knows only a scoped event publisher.
2
- import { createHash } from 'node:crypto';
3
- import { eventClient, spentClause } from '@volter/teams/event-client';
4
- import { EVENT_DATA_LIMIT } from '@volter/teams/event-limits';
5
- import { boardSourceName, publishableCard, recordCardPublications } from './published.mjs';
6
- import { listBoards, openBoard } from './store.mjs';
7
-
8
- export function boardEventRelay({root,context,supercodeHome,log=()=>{}}) {
9
- let work=null,dirty=false,closed=false,retry=null,reconcile=true,heldUntil=0;const sources=new Map();
10
- const run=async()=>{
11
- if(reconcile){
12
- // Establish every native source head before any network operation. A
13
- // delivery retry cannot leave later boards without their initial inventory.
14
- for(const slug of listBoards(root)){const board=openBoard(root,slug);try{board.tx(db=>recordCardPublications(db,null));}finally{board.close();}}
15
- reconcile=false;
16
- }
17
- const {team,context:audience}=eventClient({context,supercodeHome});
18
- for(const slug of listBoards(root)){
19
- const board=openBoard(root,slug);
20
- try{
21
- const key=JSON.stringify({server:audience.server,org:audience.org,principal:audience.principal,board:slug});
22
- let source=sources.get(key);
23
- if(!source){source=(await team.events.createSource({name:boardSourceName(root,slug)},{caller:'board-relay source'})).data.id;sources.set(key,source);}
24
- const destination=createHash('sha256').update(`${key}:${source}`).digest('hex');
25
- for(;;){
26
- const rows=board.db.prepare('SELECT * FROM supercode_publications p WHERE NOT EXISTS (SELECT 1 FROM supercode_publication_receipts r WHERE r.event_id=p.id AND r.destination=?) ORDER BY sequence LIMIT 32').all(destination);if(!rows.length)break;
27
- for(const row of rows){
28
- if(closed)return;
29
- const id=`event_${createHash('sha256').update(`${row.id}:${destination}`).digest('hex')}`;
30
- await team.events.publish(source,{id,kind:row.kind,resource:{kind:'task',id:`${source}:${row.resource_id}`},revision:row.sequence,data:{board:slug,card:publishableCard(JSON.parse(row.snapshot),slug,EVENT_DATA_LIMIT)},actor:{session:row.actor?`session:${row.actor}`:null}},{idempotencyKey:id,caller:'board-relay publish'})
31
- .catch(error=>{throw Object.assign(error,{message:`${row.resource_id} ${row.kind} (sequence ${row.sequence}): ${error.message}`});});
32
- board.tx(db=>db.prepare('INSERT OR IGNORE INTO supercode_publication_receipts VALUES (?,?)').run(row.id,destination));
33
- }
34
- }
35
- }finally{board.close();}
36
- }
37
- };
38
- const wake=(refresh=true)=>{
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;
43
- work=(async()=>{while(dirty&&!closed){dirty=false;await run();}})().catch(error=>{
44
- log(`board event delivery pending: ${error.code?`${error.code}: `:''}${error.message}${spentClause(error)}`);
45
- // Retry durable delivery, not a read of the board's current state.
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?.();
48
- }).finally(()=>{work=null;});return work;
49
- };
50
- return {wake,close(){closed=true;clearTimeout(retry);}};
51
- }