@volter/supercode-orchestrator 0.5.69 → 0.5.71
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/agent-door.mjs +75 -0
- package/agents.mjs +42 -23
- package/bin/orchestrator.mjs +1 -2
- package/board/agent-declarations.mjs +3 -1
- package/board/args.mjs +36 -0
- package/board/call.mjs +45 -0
- package/board/caller-session.mjs +25 -0
- package/board/cli.mjs +258 -121
- package/board/client.mjs +85 -0
- package/board/dispatch.mjs +70 -76
- package/board/doors.mjs +60 -0
- package/board/engine.mjs +9 -5
- package/board/event-stream.mjs +7 -7
- package/board/fence.mjs +42 -0
- package/board/files.mjs +23 -3
- package/board/git-document.mjs +2 -1
- package/board/lock-holder.mjs +49 -0
- package/board/owner-files.mjs +19 -0
- package/board/published.mjs +106 -41
- package/board/store.mjs +38 -11
- package/board/workflow.mjs +109 -13
- package/board/ztrack.mjs +24 -14
- package/hand-off.mjs +2 -2
- package/index.d.ts +1 -1
- package/kanban-worker.mjs +11 -16
- package/loop.mjs +3 -2
- package/mcp/server.mjs +12 -9
- package/package.json +2 -2
- package/bin/agent-door.mjs +0 -53
package/agent-door.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The orchestrator's door to Teams for agent identity, called in-process through the installed neutral SDK: the board's
|
|
2
|
+
// owner is resident and async, so it awaits the request itself, never through a helper process.
|
|
3
|
+
import { isCallersCall } from './board/call.mjs';
|
|
4
|
+
import { fromContext, WorkspaceContexts } from '@volter/teams/context';
|
|
5
|
+
import { supercodeHome } from '@volter/teams/home';
|
|
6
|
+
import { openMachine, normalMachineName } from '@volter/teams/machine-door';
|
|
7
|
+
import { StreamRpc } from '@volter/teams/client';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
|
|
10
|
+
// This machine is reached through its own daemon's socket, never through the Teams server: the server route lists the
|
|
11
|
+
// org's machines and opens a channel on the shared credential's allowance for a request that never leaves the box.
|
|
12
|
+
const reach = (machine) => machine && normalMachineName(machine) !== normalMachineName(hostname()) ? machine : null;
|
|
13
|
+
// A caller's verb in the board's owner names its context (the CLI and the daemon send it): an absent one is refused,
|
|
14
|
+
// never taken from the owner's own environment. The process's own work reads its environment, then the selection.
|
|
15
|
+
const contextOf = (context) => {
|
|
16
|
+
if (context) return context;
|
|
17
|
+
if (isCallersCall()) throw Object.assign(new Error('the caller named no Teams context'), { code: 'invalid_request' });
|
|
18
|
+
return process.env.SUPERCODE_CONTEXT ?? new WorkspaceContexts(supercodeHome()).read().current;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request), or
|
|
23
|
+
* this home's local store when none is selected. A context named but unreadable here is this home's error, and throws.
|
|
24
|
+
*/
|
|
25
|
+
export function agentScope(context) {
|
|
26
|
+
const named = contextOf(context);
|
|
27
|
+
if (!named || named === 'local') return { server: 'local', org: 'local', principal: null, offline: true, reason: 'no Teams context is selected' };
|
|
28
|
+
const { context: scope } = fromContext({ supercodeHome: supercodeHome(), name: named });
|
|
29
|
+
return { server: scope.origin, server_id: scope.server_id, org: scope.team_id, principal: scope.principal_id };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One agent-identity request to the selected context's Teams server. Throws its refusal, with its status and code. */
|
|
33
|
+
export async function agentDoor(op, args = [], { context, timeout = 25_000 } = {}) {
|
|
34
|
+
if (op === 'scope') return agentScope(context);
|
|
35
|
+
const named = contextOf(context);
|
|
36
|
+
const { client, context: scope } = fromContext({ supercodeHome: supercodeHome(), name: named });
|
|
37
|
+
const team = client.team(scope.team_id);
|
|
38
|
+
// the door names itself to the server's tally of who spent the credential's allowance (X-Supercode-Caller)
|
|
39
|
+
const options = { signal: AbortSignal.timeout(timeout), caller: `agent-door ${op}` };
|
|
40
|
+
switch (op) {
|
|
41
|
+
case 'root': return team.agents.root(options);
|
|
42
|
+
case 'get': return team.agents.get(args[0], options);
|
|
43
|
+
case 'management': return team.agents.management(args[0], options);
|
|
44
|
+
case 'profile': return team.agents.update(args[0], { profile: args[1] }, { ...options, ifMatch: args[2] });
|
|
45
|
+
case 'reparent': return team.agents.update(args[0], { parent: args[1] }, { ...options, ifMatch: args[2] });
|
|
46
|
+
case 'declare': return team.agents.declare(args[0], { ...options, idempotencyKey: args[1] });
|
|
47
|
+
case 'sessions': return team.agents.sessions(args[0], args[1] ?? {}, options);
|
|
48
|
+
case 'ownSession': return team.agents.ownSession(args[0], args[1], { ...options, idempotencyKey: args[2] });
|
|
49
|
+
case 'session': return team.sessions.get(args[0]).identity(options);
|
|
50
|
+
case 'end': {
|
|
51
|
+
const agent = (await team.agents.get(args[0], options)).data;
|
|
52
|
+
return agent.state === 'ended' ? { data: { id: agent.id, state: 'ended' } } : team.agents.end(agent.id, { reason: args[1] }, { ...options, ifMatch: agent.revision, idempotencyKey: args[2] });
|
|
53
|
+
}
|
|
54
|
+
case 'reader': return team.agents.delegateReader(args[0], {}, { ...options, ifMatch: args[1] });
|
|
55
|
+
case 'readerContext': {
|
|
56
|
+
const identity = await team.agents.get(args[0], options);
|
|
57
|
+
const reader = await team.agents.delegateReader(args[0], { expires_in_ms: 86_400_000 }, { ...options, ifMatch: identity.data.revision });
|
|
58
|
+
const channel = await openMachine({ machine: reach(args[1]), context: named });
|
|
59
|
+
try { return await new StreamRpc(await channel.open('node')).call('harness.v1.agents.context', { origin: scope.origin, team: scope.team_id, agent: args[0], credential: reader.data }); }
|
|
60
|
+
finally { channel.close(); }
|
|
61
|
+
}
|
|
62
|
+
case 'resolveIdentity': return team.identity.resolve(args[0], options);
|
|
63
|
+
case 'find': {
|
|
64
|
+
let after = '';
|
|
65
|
+
do {
|
|
66
|
+
const page = await team.agents.list({ after, limit: 200 }, options);
|
|
67
|
+
const found = page.items.find((agent) => agent.name === args[0] && agent.parent === args[1] && agent.state !== 'ended') ?? null;
|
|
68
|
+
if (found) return { data: found };
|
|
69
|
+
after = page.next_after;
|
|
70
|
+
} while (after);
|
|
71
|
+
return { data: null };
|
|
72
|
+
}
|
|
73
|
+
default: throw Object.assign(new Error('unsupported agent door'), { code: 'invalid_request' });
|
|
74
|
+
}
|
|
75
|
+
}
|
package/agents.mjs
CHANGED
|
@@ -2,28 +2,15 @@
|
|
|
2
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
3
|
import { mkdirSync } from 'node:fs';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
8
6
|
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
9
7
|
import { resolveAgentProfile } from './agent-profiles.mjs';
|
|
10
8
|
import { compareAgentProfile } from '@volter/teams/agent-profile';
|
|
11
9
|
import { WorkspaceContexts } from '@volter/teams/context';
|
|
12
10
|
import { supercodeHome } from '@volter/teams/home';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const lastJson=text=>{for(const line of String(text??'').trim().split('\n').reverse()){try{const value=JSON.parse(line);if(value&&typeof value==='object')return value;}catch{/* not the door's line */}}return null;};
|
|
17
|
-
export function agentDoor(op,args=[],{context,timeout=25000}={}){
|
|
18
|
-
const reply=spawnSync(process.execPath,[entry],{input:JSON.stringify({op,args,context}),encoding:'utf8',timeout,maxBuffer:4*1024*1024,env:{...process.env,SUPERCODE_CALLER_NAME:`${HOST_CALLER} agent-door ${op}`}});
|
|
19
|
-
// The door's answer to a failure is the last stderr line that is JSON: a runtime's own lines (a preload's notice, a
|
|
20
|
-
// Node warning) may come before or after it.
|
|
21
|
-
if(reply.status!==0){const detail=lastJson(reply.stderr)??{message:reply.error?.message??String(reply.stderr).slice(-1000)};throw Object.assign(new Error(detail.message??'Teams agent door unavailable'),detail);}
|
|
22
|
-
return JSON.parse(reply.stdout);
|
|
23
|
-
}
|
|
24
|
-
/** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
|
|
25
|
-
* or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
|
|
26
|
-
export function agentScope(context){return agentDoor('scope',[],{context});}
|
|
11
|
+
// Agent identity is Teams's (agent-door.mjs, in-process); its intent and assignments are this home's (below).
|
|
12
|
+
export { agentDoor, agentScope } from './agent-door.mjs';
|
|
13
|
+
import { agentDoor, agentScope } from './agent-door.mjs';
|
|
27
14
|
function store(root){
|
|
28
15
|
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
|
|
29
16
|
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
@@ -50,13 +37,13 @@ export function assignAgent(root,org,id,{kind,target,role}){
|
|
|
50
37
|
const db=store(root),key=digest(JSON.stringify([kind,target,role]));try{db.prepare("INSERT INTO agent_assignments(org,id,agent_id,kind,target,role,state,started_at) VALUES (?,?,?,?,?,?,'active',?) ON CONFLICT(org,id) DO UPDATE SET agent_id=excluded.agent_id,state='active',ended_at=NULL").run(org,key,id,kind,target,role,Date.now());return key;}finally{db.close();}
|
|
51
38
|
}
|
|
52
39
|
export function activeAgentAssignments(root){const db=store(root);try{return db.prepare("SELECT * FROM agent_assignments WHERE state='active'").all();}finally{db.close();}}
|
|
53
|
-
export function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
54
|
-
const target=agentDoor('management',[to],{context}).data;
|
|
40
|
+
export async function reassignAgent(root,org,assignment,to,{context}={}) {
|
|
41
|
+
const target=(await agentDoor('management',[to],{context})).data;
|
|
55
42
|
const db=store(root);
|
|
56
43
|
try {
|
|
57
44
|
const old=db.prepare("SELECT * FROM agent_assignments WHERE org=? AND id=? AND state='active'").get(org,assignment);
|
|
58
45
|
if(!old)throw new Error('active assignment not found');
|
|
59
|
-
if(old.agent_id!==to)agentDoor('management',[old.agent_id],{context});
|
|
46
|
+
if(old.agent_id!==to)await agentDoor('management',[old.agent_id],{context});
|
|
60
47
|
db.exec('BEGIN IMMEDIATE');
|
|
61
48
|
const moved=db.prepare("UPDATE agent_assignments SET agent_id=? WHERE org=? AND id=? AND agent_id=? AND state='active'").run(to,org,assignment,old.agent_id);
|
|
62
49
|
if(moved.changes!==1)throw new Error('assignment changed while reassigning it');
|
|
@@ -224,10 +211,43 @@ export function localForkAgent(options){
|
|
|
224
211
|
* socket), which declares and ends them in Teams (D137: identity is Teams' job, never the dispatcher's or the gateway's).
|
|
225
212
|
* Local only: a daemon not running keeps them here for the next hand-off. Answers what could not be handed on.
|
|
226
213
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
214
|
+
/**
|
|
215
|
+
* The Teams context this machine has for `org`: the one context of that team that is not an agent's delegated reader
|
|
216
|
+
* context (`agent-…`); among several, the selected one. Null when there is none, or several and none selected: the
|
|
217
|
+
* caller says so rather than taking another org's or the process's.
|
|
218
|
+
*/
|
|
219
|
+
function contextForOrg(org){
|
|
220
|
+
const doc=new WorkspaceContexts(supercodeHome()).read();
|
|
221
|
+
const names=Object.entries(doc.contexts??{}).filter(([name,value])=>value?.team_id===org&&!name.startsWith('agent-')).map(([name])=>name);
|
|
222
|
+
return names.length===1?names[0]:names.includes(doc.current)?doc.current:null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Hand this home's pending agent declarations and ends to Teams, each org's through this machine's context for that org
|
|
227
|
+
* (the context named, for its org alone). Never the process's own environment: the work was recorded for an org, and
|
|
228
|
+
* that org's context is the one that delivers it.
|
|
229
|
+
*/
|
|
230
|
+
export function handOffAgents(root,{context=null}={}){
|
|
230
231
|
const waiting=[];
|
|
232
|
+
if(context){const scope=agentScope(context);if(!scope.offline)handOffOrg(root,scope,waiting);return waiting;}
|
|
233
|
+
let orgs;
|
|
234
|
+
const db=store(root);
|
|
235
|
+
try{orgs=db.prepare("SELECT org FROM agent_intents WHERE declared=0 AND declaration_json IS NOT NULL UNION SELECT org FROM agent_end_outbox").all().map(row=>row.org).filter(org=>org&&org!=='local');}
|
|
236
|
+
finally{db.close();}
|
|
237
|
+
for(const org of orgs){
|
|
238
|
+
// one org whose context cannot be read (a saved context gone bad) is said and left; the others are still handed off
|
|
239
|
+
try{
|
|
240
|
+
const named=contextForOrg(org);
|
|
241
|
+
if(!named){waiting.push({agent:'*',error:`no Teams context on this machine for org ${org} (or several, none selected); its agent work waits`});continue;}
|
|
242
|
+
const scope=agentScope(named);
|
|
243
|
+
if(!scope.offline&&scope.org===org)handOffOrg(root,scope,waiting);
|
|
244
|
+
}catch(error){waiting.push({agent:'*',error:`org ${org}: ${error.message}; its agent work waits`});}
|
|
245
|
+
}
|
|
246
|
+
return waiting;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handOffOrg(root,scope,waiting){
|
|
250
|
+
adoptLocalOrg(root,scope);
|
|
231
251
|
let db=store(root),rows,ends;
|
|
232
252
|
try{
|
|
233
253
|
rows=db.prepare('SELECT * FROM agent_intents WHERE org=? AND declared=0 AND declaration_json IS NOT NULL ORDER BY created_at').all(scope.org);
|
|
@@ -254,7 +274,6 @@ export function handOffAgents(root,{context=process.env.SUPERCODE_CONTEXT}={}){
|
|
|
254
274
|
db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(scope.org,row.agent_id);
|
|
255
275
|
}finally{db.close();}
|
|
256
276
|
}
|
|
257
|
-
return waiting;
|
|
258
277
|
}
|
|
259
278
|
/** A managed runtime may report its native id after opening. Keep the lease pointer out of the catalogue. */
|
|
260
279
|
export function recordNativeAgentSession(root,runtimeRef,session) {
|
package/bin/orchestrator.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
// Board commands do not load daemon transports, platform adapters or the MCP server.
|
|
11
11
|
if (process.argv[2] === 'workflow') {
|
|
12
|
-
const [{runWorkflow},{
|
|
12
|
+
const [{runWorkflow},{timeCommand}]=await Promise.all([import('../board/client.mjs'),import('@volter/supercode-harness-sdk/slow-log')]);
|
|
13
13
|
// A board command is timed into the slow log; `serve` runs until stopped and is not one.
|
|
14
14
|
const verb=process.argv.slice(3).find((arg,index,all)=>!arg.startsWith('-')&&!(index>0&&/^--(root|board|cwd|context|at)$/.test(all[index-1])));
|
|
15
15
|
if(verb!=='serve')timeCommand(`workflow ${verb??''}`.trim(),process.argv.slice(3));
|
|
@@ -18,7 +18,6 @@ if (process.argv[2] === 'workflow') {
|
|
|
18
18
|
// a store's own error (SQLite's, with its errcode) is no message written for a person: its stack says which statement
|
|
19
19
|
// met it, as a follower's end in the connector's log (board events) otherwise could not
|
|
20
20
|
catch(error) {process.stderr.write(`supercode workflow: ${error.message??error}\n${error?.errcode!=null&&error.stack?`${error.stack}\n`:''}`);code=1;}
|
|
21
|
-
finally {await closeWorld();}
|
|
22
21
|
await Promise.all([process.stdout,process.stderr].map(stream=>new Promise(done=>stream.write('',done))));
|
|
23
22
|
process.exit(code);
|
|
24
23
|
} else if (process.argv[2] === 'agent-channel') {
|
|
@@ -14,7 +14,9 @@ export function declareCardAgentEffect(board,effect) {
|
|
|
14
14
|
const told=view(board,db=>new Set(db.prepare("SELECT payload FROM task_events WHERE task_id=? AND kind='agent_declaration_pending' AND id>COALESCE((SELECT max(id) FROM task_events WHERE task_id=? AND kind='agent_declared'),0)").all(effect.id,effect.id).map(row=>json(row.payload,{})?.reason)));
|
|
15
15
|
try {
|
|
16
16
|
if(!entry.profiles[profile])throw new Error('card agent declaration needs a named profile');
|
|
17
|
-
|
|
17
|
+
// the context the card's creator named, kept with the declaration: never the context of whoever retries it
|
|
18
|
+
if(!effect.context)throw new Error('the card agent declaration recorded no Teams context; it is declared again by a call that names one');
|
|
19
|
+
const context=effect.context,scope=agentScope(context);
|
|
18
20
|
const options={root:board.root,org:scope.org,server:scope.server,profiles:entry.profiles,
|
|
19
21
|
profile,card:effect.id,role:effect.role,sponsor:task.sponsor??null,context,starterSession:effect.starter_session};
|
|
20
22
|
const agent=localCardAgent(options);
|
package/board/args.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// A workflow verb's arguments, as the CLI and the board's owner both read them.
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const MULTI = new Set(['parent', 'skill', 'worker', 'child', 'ids', 'rm', 'tag', 'pr', 'pr-head', 'reviewed-sha', 'finding']);
|
|
5
|
+
export const FLAGS = new Set(['json', 'triage', 'no-notify', 'no-start', 'all', 'force', 'help', 'mine', 'archived', 'reclaim', 'dry-run',
|
|
6
|
+
'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose', 'commit']);
|
|
7
|
+
// flags that take every following word up to the next flag (Hermes's nargs='+')
|
|
8
|
+
export const GREEDY = new Set(['ids', 'rm']);
|
|
9
|
+
|
|
10
|
+
export function parse(argv) {
|
|
11
|
+
const out = { _: [] };
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (!a.startsWith('--') || a === '--') { out._.push(a); continue; }
|
|
15
|
+
const [key, inline] = a.slice(2).split(/=(.*)/s);
|
|
16
|
+
if (FLAGS.has(key)) { out[key] = true; continue; }
|
|
17
|
+
if (GREEDY.has(key)) {
|
|
18
|
+
const values = inline !== undefined ? [inline] : [];
|
|
19
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) values.push(argv[++i]);
|
|
20
|
+
(out[key] ??= []).push(...values);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const value = inline ?? argv[++i];
|
|
24
|
+
if (value === undefined) throw new Error(`--${key} needs a value`);
|
|
25
|
+
if (MULTI.has(key)) (out[key] ??= []).push(value);
|
|
26
|
+
else out[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The home a verb names: `--root`, else the home's own variable. */
|
|
32
|
+
export function homeOf(args) {
|
|
33
|
+
const root = args.root ?? process.env.SUPERCODE_ORCHESTRATOR_HOME;
|
|
34
|
+
if (!root) throw new Error('which home? --root <home> (or SUPERCODE_ORCHESTRATOR_HOME)');
|
|
35
|
+
return resolve(root);
|
|
36
|
+
}
|
package/board/call.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The call a board verb answers: who it answers for and where its answer goes. A board's resident owner (`workflow
|
|
2
|
+
// serve`) answers many callers' verbs at once, each a call of its own, so nothing that names the caller or writes the
|
|
3
|
+
// answer reads it from the process: it reads the call it runs in. A process's own work (the owner's dispatching, an
|
|
4
|
+
// operation verb run on the machine) runs in the process's own call.
|
|
5
|
+
//
|
|
6
|
+
// caller the calling session's address (`sc:<machine>:<harness>:<id>`), as the caller's own side named it, or null
|
|
7
|
+
// facts what the caller's environment says of it: SUPERCODE_AGENT_ID, SUPERCODE_AUTOMATION, SUPERCODE_CONTEXT,
|
|
8
|
+
// HERMES_PROFILE, USER (the process's own environment in its own call)
|
|
9
|
+
// actor, principal, roles, recorded what the verb learns of the caller as it runs (store.mjs, engine.mjs, cli.mjs)
|
|
10
|
+
// said the standing warnings this call has already been told (a backing's, once per call)
|
|
11
|
+
// cwd the caller's working directory, against which a path it names is read
|
|
12
|
+
// stdin what the caller sent on its standard input, for a `-` it names as a file (null: none)
|
|
13
|
+
// out, err where the verb writes its answer
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
|
+
|
|
17
|
+
const calls = new AsyncLocalStorage();
|
|
18
|
+
// what a call learns as it runs, and the workflow it read (pinned: a reload while it runs does not change its basis)
|
|
19
|
+
const fresh = () => ({ actor: null, principal: null, roles: () => ['manager'], recorded: null, workflows: new Map(), said: new Set() });
|
|
20
|
+
// the process's own context pins no workflow (it lives as long as the process): its reads always take the latest
|
|
21
|
+
const unpinned = { get: () => undefined, set: () => {} };
|
|
22
|
+
const own = { kind: 'own', caller: null, facts: process.env, ...fresh(), workflows: unpinned, get cwd() { return process.cwd(); }, get stdin() { return readFileSync(0, 'utf8'); }, out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) };
|
|
23
|
+
|
|
24
|
+
/** The call running now: a caller's verb in the owner, else the process's own. */
|
|
25
|
+
export const current = () => calls.getStore() ?? own;
|
|
26
|
+
|
|
27
|
+
/** Run `fn` as one caller's call, its answer written through `out` and `err`. */
|
|
28
|
+
export function within({ caller = null, facts = {}, cwd, stdin = null, out, err }, fn) {
|
|
29
|
+
return calls.run({ kind: 'caller', caller, facts, ...fresh(), cwd, stdin, out, err }, fn);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Run `fn` as a unit of the process's own work (a round, a delivery pass): the process's identity and output, with
|
|
33
|
+
* what it learns and the workflow it read kept to itself. */
|
|
34
|
+
export function ownCall(fn) {
|
|
35
|
+
return calls.run({ kind: 'own', caller: null, facts: process.env, ...fresh(), get cwd() { return process.cwd(); }, stdin: null, out: own.out, err: own.err }, fn);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The caller as its address names it, or null. */
|
|
39
|
+
export function callerOf(call = current()) {
|
|
40
|
+
const m = /^sc:([^:\s]+):(claude-code|codex):([^:\s]+)$/.exec(call.caller ?? '');
|
|
41
|
+
return m ? { address: m[0], machine: m[1], harness: m[2], session: m[3] } : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether this is a caller's call in the owner rather than the process's own. */
|
|
45
|
+
export const isCallersCall = () => calls.getStore()?.kind === 'caller';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The session this process runs in, as its own harness names it (Claude Code CLAUDE_CODE_SESSION_ID, Codex
|
|
2
|
+
// CODEX_THREAD_ID). A pane inherits every variable the program that started its terminal server had, so a Codex pane
|
|
3
|
+
// can carry a Claude session's id too: the nearest harness among the process's ancestors says which is its own.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export function processSession() {
|
|
8
|
+
const claude = process.env.CLAUDE_CODE_SESSION_ID || null;
|
|
9
|
+
const codex = process.env.CODEX_THREAD_ID || null;
|
|
10
|
+
if (!claude || !codex) return claude ?? codex;
|
|
11
|
+
const table = spawnSync('ps', ['-A', '-o', 'pid=,ppid=,comm='], { encoding: 'utf8' });
|
|
12
|
+
const processes = new Map();
|
|
13
|
+
for (const row of String(table.stdout ?? '').split('\n')) {
|
|
14
|
+
const m = row.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
15
|
+
if (m) processes.set(Number(m[1]), { parent: Number(m[2]), name: basename(m[3]) });
|
|
16
|
+
}
|
|
17
|
+
for (let pid = process.ppid, depth = 0; pid > 1 && depth < 64; depth++) {
|
|
18
|
+
const p = processes.get(pid);
|
|
19
|
+
if (!p) break;
|
|
20
|
+
if (p.name === 'codex') return codex;
|
|
21
|
+
if (/^claude(\.exe)?$/.test(p.name)) return claude;
|
|
22
|
+
pid = p.parent;
|
|
23
|
+
}
|
|
24
|
+
return claude;
|
|
25
|
+
}
|