@volter/supercode-orchestrator 0.5.214 → 0.5.216
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/README.md +71 -9
- package/activation.mjs +47 -11
- package/adapters/chat-sdk.mjs +82 -15
- package/adapters/rh2.mjs +11 -7
- package/agents.mjs +1 -5
- package/bin/agent-channel.mjs +33 -177
- package/bin/connection-runner.mjs +3 -17
- package/bin/orchestrator.mjs +12 -7
- package/bin/runtime.mjs +74 -51
- package/board/call.mjs +2 -1
- package/board/ci.mjs +55 -28
- package/board/cli.mjs +117 -13
- package/board/client.mjs +9 -0
- package/board/dispatch.mjs +45 -15
- package/board/engine.mjs +2 -2
- package/board/fence.mjs +2 -0
- package/board/generation-owner.mjs +293 -0
- package/board/generation.mjs +49 -0
- package/board/listing.mjs +17 -5
- package/board/owner-door-worker.mjs +22 -7
- package/board/owner-door.mjs +2 -2
- package/board/owner-files.mjs +1 -1
- package/board/published.mjs +12 -7
- package/board/reply.mjs +2 -2
- package/board/session-work.mjs +85 -15
- package/connection.mjs +14 -20
- package/cron.mjs +57 -4
- package/daemon-control.d.ts +39 -0
- package/daemon-control.mjs +275 -0
- package/hermes-locks.mjs +13 -3
- package/index.d.ts +9 -4
- package/installed-apps.mjs +35 -14
- package/kanban-worker.mjs +4 -4
- package/lease.mjs +1 -1
- package/loop.mjs +198 -47
- package/mcp/mount.mjs +6 -5
- package/orchestration.mjs +11 -17
- package/package.json +10 -7
- package/reducer.mjs +31 -6
- package/source-lock.mjs +15 -24
- package/webhook.mjs +65 -12
package/bin/agent-channel.mjs
CHANGED
|
@@ -1,199 +1,55 @@
|
|
|
1
|
-
// `supercode-orchestrator agent-channel --agent <name
|
|
2
|
-
// (docs/adr/0008-agent-mailbox.md
|
|
3
|
-
// channel
|
|
4
|
-
// and its client's ticket comments are channels of its account manager):
|
|
1
|
+
// `supercode-orchestrator agent-channel --agent <name> --platform slack|jira`: a channel carried to an agent's one mailbox
|
|
2
|
+
// (docs/adr/0008-agent-mailbox.md) through the mailbox bridge (company RFC 0026 decision 3: an engagement's Slack
|
|
3
|
+
// channel and its client's ticket comments are channels of its account manager):
|
|
5
4
|
// --platform slack --slack-channel <id>[,<id>] the Chat SDK's Slack (Socket Mode), SLACK_BOT_TOKEN and SLACK_APP_TOKEN
|
|
6
5
|
// from the environment; only the named channels' lines are filed
|
|
7
6
|
// --platform jira --site <url> --jql <jql> the tickets' comments (adapters/jira-comments.mjs), JIRA_EMAIL and
|
|
8
7
|
// JIRA_API_TOKEN (or JIRA_TOKEN) from the environment
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// and every answer is posted back anchored at its thread's root, by `AgentMailboxBridge` over the RH2 adapter.
|
|
12
|
-
//
|
|
13
|
-
// The Room is found by its key (`--room-key`, default `account-<agent>`), or opened once when the organization has
|
|
14
|
-
// none: from the organization's preset (`--preset`) with the principal (`--principal`, RH2's principal id) and the agent
|
|
15
|
-
// both seated in the role the org's floor names (`--role`), and the agent answering every line of its conversation.
|
|
16
|
-
// Authority comes from a stated role: opening a Room without both is refused. `--room <id>` names an existing Room
|
|
17
|
-
// instead and opens nothing.
|
|
18
|
-
//
|
|
19
|
-
// RH2 is reached as the agent's own principal where the organization declares the agent (docs/adr/0017: `supercode
|
|
20
|
-
// orchestrator apps install rh2-agent … --agent <its Teams id>` in the home, `--root` or HERMES_HOME), the record bound to
|
|
21
|
-
// the agent this channel names with `--agent-id`, else to the id the home holds for that agent (homeAgentId: its declared
|
|
22
|
-
// identity, else its profile's by the home's agent layer `agents.json`), so its threads and its call are one participant
|
|
23
|
-
// and no other agent's record is taken; otherwise through the Room-chat app's installation for this agent (`apps install
|
|
24
|
-
// room-chat … --profile <agent>`). The record's origin, organization, principal and token are the agent's identity in the
|
|
25
|
-
// Room. The bridge's thread anchors are kept in `--state`.
|
|
26
|
-
//
|
|
27
|
-
// The agent's seat in its Room carries its main session (RFC 0020 decision 26): where this machine is in a Teams team,
|
|
28
|
-
// the seat is bound to the main session (`supercode sessions bind`, the session owner's view binding for the seat), and
|
|
29
|
-
// rebound to each new main session, as the Agents → Room agents app binds an arc's agent. Until the main session is in
|
|
30
|
-
// the team's catalog the binding is retried.
|
|
8
|
+
// An agent's RH2 Rooms are not carried here: the Workplace's join command brings the agent in, its seat carries the
|
|
9
|
+
// agent's current session, and the Workplace delivers the Room's lines to that session itself (runhuman-2 ADR 0063).
|
|
31
10
|
import { parseArgs } from 'node:util';
|
|
32
|
-
import { execFileSync } from 'node:child_process';
|
|
33
|
-
import { readFileSync } from 'node:fs';
|
|
34
11
|
import { homedir } from 'node:os';
|
|
35
12
|
import { join } from 'node:path';
|
|
36
13
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
37
|
-
import { Rh2Adapter } from '../adapters/rh2.mjs';
|
|
38
14
|
import { AgentMailboxBridge } from '../adapters/agent-mailbox.mjs';
|
|
39
15
|
import { JiraCommentsAdapter } from '../adapters/jira-comments.mjs';
|
|
40
16
|
import { ChatSdkAdapter } from '../adapters/chat-sdk.mjs';
|
|
41
|
-
import { installedAgentRecord, installedApps } from '../installed-apps.mjs';
|
|
42
|
-
import { agentScope, homeAgentId } from '../agents.mjs';
|
|
43
17
|
|
|
44
18
|
const { values } = parseArgs({
|
|
45
19
|
args: process.argv.slice(3),
|
|
46
|
-
options: { agent: { type: 'string' },
|
|
20
|
+
options: { agent: { type: 'string' }, state: { type: 'string' }, platform: { type: 'string' }, site: { type: 'string' }, jql: { type: 'string' }, 'slack-channel': { type: 'string' }, help: { type: 'boolean' } },
|
|
47
21
|
});
|
|
48
|
-
if (values.help || !values.agent) {
|
|
49
|
-
console.log('supercode-orchestrator agent-channel --agent <name>
|
|
22
|
+
if (values.help || !values.agent || !values.platform) {
|
|
23
|
+
console.log('supercode-orchestrator agent-channel --agent <name> --platform slack --slack-channel <id>[,<id>] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform jira --site <url> --jql <jql> [--state <file>]');
|
|
50
24
|
process.exit(values.help ? 0 : 2);
|
|
51
25
|
}
|
|
52
26
|
const log = (level, line) => (level === 'debug' ? undefined : console.error(`${new Date().toISOString()} ${level} ${line}`));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (values.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} else { console.error(`agent-channel: --platform is rh2, slack or jira (got ${values.platform})`); process.exit(2); }
|
|
76
|
-
const client = new SupercodeHarnessClient(process.env.SUPERCODE_BIN ? { command: process.env.SUPERCODE_BIN } : {});
|
|
77
|
-
const bridge = new AgentMailboxBridge({ adapter, client, agent: values.agent, platform: values.platform, fromName: `${values.platform}-${values.agent}`, log, statePath: statePath(values.platform) });
|
|
78
|
-
await bridge.start();
|
|
79
|
-
log('info', `${values.agent}: ${values.platform} is carried to its mailbox`);
|
|
80
|
-
const stop = async () => { await bridge.stop(); await adapter.close?.(); await client.close?.(); process.exit(0); };
|
|
81
|
-
process.on('SIGTERM', stop);
|
|
82
|
-
process.on('SIGINT', stop);
|
|
83
|
-
} else {
|
|
84
|
-
const root = values.root ?? process.env.HERMES_HOME;
|
|
85
|
-
// The agent's own record only when it is this agent's (bound to its Teams id, as an agent's sessions are mounted with it):
|
|
86
|
-
// the id named, else the one this home holds for the agent, read and never minted.
|
|
87
|
-
const agentId = values['agent-id'] ?? (root ? (() => {
|
|
88
|
-
try {
|
|
89
|
-
// a named profile implies its own agent: a home with no agent layer runs the agent on the profile of its name
|
|
90
|
-
let layer = {};
|
|
91
|
-
try { layer = JSON.parse(readFileSync(join(root, 'agents.json'), 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
92
|
-
const { org, server } = agentScope(process.env.SUPERCODE_CONTEXT);
|
|
93
|
-
return homeAgentId(root, { org, server, name: values.agent, profile: layer.agents?.[values.agent]?.profile ?? values.agent });
|
|
94
|
-
} catch (error) { log('warn', `${values.agent}: its id in ${root} was not read (${error.message})`); return null; }
|
|
95
|
-
})() : null);
|
|
96
|
-
const own = root && agentId ? installedAgentRecord(root, agentId) : null;
|
|
97
|
-
const installation = own ?? (root ? installedApps(root).find((app) => app.app === 'room-chat' && app.profile === values.agent) : undefined);
|
|
98
|
-
if (!installation) {
|
|
99
|
-
console.error(`agent-channel: ${values.agent} has no RH2 identity in ${root ?? '(no home: --root or HERMES_HOME)'}; install its agent (supercode orchestrator apps install rh2-agent … --agent ${agentId ?? '<its Teams id>, and run this with --agent-id <its Teams id>'}) or Room chat for it (apps install room-chat … --profile ${values.agent})`);
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
const identity = { api_url: installation.origin, organization: installation.organizationId, principal_id: installation.principalId, token: installation.token };
|
|
103
|
-
const adapter = new Rh2Adapter({ log, channel: { enabled: true, extra: identity } });
|
|
104
|
-
/** One RH2 write, its `data` returned; a refusal throws with its status. */
|
|
105
|
-
async function post(path, body) {
|
|
106
|
-
const res = await adapter.post(path, body);
|
|
107
|
-
if (!res.ok) throw new Error(`POST ${path} -> HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
108
|
-
return (await res.json()).data ?? {};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** The account Room: named, found by its key, or opened once. */
|
|
112
|
-
async function accountRoom() {
|
|
113
|
-
if (values.room) return values.room;
|
|
114
|
-
const key = values['room-key'] ?? `account-${values.agent}`;
|
|
115
|
-
const listing = await adapter.get('/rooms');
|
|
116
|
-
const found = (Array.isArray(listing) ? listing : listing.rooms ?? []).map((entry) => entry.room ?? entry).find((room) => room?.key === key && room.state === 'active');
|
|
117
|
-
if (found) return String(found.roomId);
|
|
118
|
-
if (!values.principal) throw new Error(`no active Room with key ${key}, and no --principal to open it for`);
|
|
119
|
-
// the Room's shape and the seats' role are the organization's (its floor names them), never this door's
|
|
120
|
-
const missing = ['preset', 'role'].filter((name) => !values[name]);
|
|
121
|
-
if (missing.length) throw new Error(`no active Room with key ${key}; opening one needs ${missing.map((name) => `--${name}`).join(' and ')} from the organization's floor (the preset the Room is made from and the role its principal and agent hold)`);
|
|
122
|
-
const seated = [...new Set([values.principal, adapter.principalId()])];
|
|
123
|
-
const { room } = await post('/rooms', {
|
|
124
|
-
key,
|
|
125
|
-
presetId: values.preset,
|
|
126
|
-
name: values['room-name'] ?? `${values.agent}`,
|
|
127
|
-
members: seated.map((principalId) => ({ principalId, roleKeys: [values.role] })),
|
|
128
|
-
});
|
|
129
|
-
const view = await adapter.get(`/rooms/${encodeURIComponent(room.roomId)}`);
|
|
130
|
-
const conversation = view.room?.defaultConversationId;
|
|
131
|
-
if (!conversation) throw new Error(`Room ${room.roomId} was opened without a conversation`);
|
|
132
|
-
await post(`/rooms/${encodeURIComponent(room.roomId)}/conversations/${encodeURIComponent(conversation)}/participants`, { answers: 'all', principalId: adapter.principalId() });
|
|
133
|
-
log('info', `opened the account Room ${room.roomId} (${key}) for ${values.agent}`);
|
|
134
|
-
return String(room.roomId);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const roomId = await accountRoom();
|
|
138
|
-
// The agent answers every line of its Room's conversation (the Room shows it as the conversation's agent); a seat it
|
|
139
|
-
// already holds answers 409.
|
|
140
|
-
{
|
|
141
|
-
const view = await adapter.get(`/rooms/${encodeURIComponent(roomId)}`);
|
|
142
|
-
const conversation = view.room?.defaultConversationId;
|
|
143
|
-
if (conversation) {
|
|
144
|
-
const res = await adapter.post(`/rooms/${encodeURIComponent(roomId)}/conversations/${encodeURIComponent(conversation)}/participants`, { answers: 'all', principalId: adapter.principalId() });
|
|
145
|
-
if (!res.ok && res.status !== 409) log('warn', `could not answer every line of ${conversation}: HTTP ${res.status}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// The adapter follows only this Room.
|
|
149
|
-
adapter.channel = { enabled: true, extra: { ...identity, rooms: [roomId] } };
|
|
27
|
+
const statePath = (name) => values.state ?? join(homedir(), '.config', 'supercode', 'agent-channels', `${values.agent}.${name}.json`);
|
|
28
|
+
let adapter;
|
|
29
|
+
if (values.platform === 'jira') {
|
|
30
|
+
if (!values.site || !values.jql) { console.error('agent-channel: --platform jira needs --site and --jql'); process.exit(2); }
|
|
31
|
+
const authorization = process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN
|
|
32
|
+
? `Basic ${Buffer.from(`${process.env.JIRA_EMAIL}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`
|
|
33
|
+
: process.env.JIRA_TOKEN ? `Bearer ${process.env.JIRA_TOKEN}` : null;
|
|
34
|
+
if (!authorization) { console.error('agent-channel: --platform jira needs JIRA_EMAIL and JIRA_API_TOKEN (or JIRA_TOKEN) in its environment'); process.exit(2); }
|
|
35
|
+
adapter = new JiraCommentsAdapter({ site: values.site, jql: values.jql, authorization, statePath: `${statePath('jira')}.comments`, log });
|
|
36
|
+
} else if (values.platform === 'slack') {
|
|
37
|
+
const channels = new Set(String(values['slack-channel'] ?? '').split(',').map((c) => c.trim()).filter(Boolean));
|
|
38
|
+
if (!channels.size) { console.error('agent-channel: --platform slack needs --slack-channel <id>: an agent is carried only the channels named'); process.exit(2); }
|
|
39
|
+
if (!process.env.SLACK_BOT_TOKEN || !process.env.SLACK_APP_TOKEN) { console.error('agent-channel: --platform slack needs SLACK_BOT_TOKEN and SLACK_APP_TOKEN in its environment'); process.exit(2); }
|
|
40
|
+
// The Slack SDK adapter is built here, in Socket Mode, from this process's own environment: the wrapper's own path
|
|
41
|
+
// reads a profile's .env, and a channel process has no profile folder. SLACK_API_URL (a twin's) is honoured.
|
|
42
|
+
let createSlackAdapter;
|
|
43
|
+
try { ({ createSlackAdapter } = await import('@chat-adapter/slack')); } catch (error) { console.error(`agent-channel: @chat-adapter/slack is not installed (${error.message})`); process.exit(2); }
|
|
44
|
+
const sdkAdapter = createSlackAdapter({ botToken: process.env.SLACK_BOT_TOKEN, appToken: process.env.SLACK_APP_TOKEN, mode: 'socket', ...(process.env.SLACK_API_URL ? { apiUrl: process.env.SLACK_API_URL } : {}) });
|
|
45
|
+
const slack = new ChatSdkAdapter({ platform: 'slack', profile: values.agent, channel: { enabled: true, extra: {} }, sdkAdapter, log });
|
|
46
|
+
// Only the named channels' lines reach the mailbox; answers go back through the same adapter.
|
|
47
|
+
adapter = { platform: 'slack', connect: (emit) => slack.connect((event) => { if (channels.has(String(event.key?.chat_id))) emit(event); }), send: (target, content) => slack.send(target, content), close: () => slack.close?.() };
|
|
48
|
+
} else { console.error(`agent-channel: --platform is slack or jira (got ${values.platform})`); process.exit(2); }
|
|
150
49
|
const client = new SupercodeHarnessClient(process.env.SUPERCODE_BIN ? { command: process.env.SUPERCODE_BIN } : {});
|
|
151
|
-
const bridge = new AgentMailboxBridge({
|
|
152
|
-
adapter, client, agent: values.agent, fromName: `rh2-${values.agent}`, log,
|
|
153
|
-
statePath: values.state ?? join(homedir(), '.config', 'supercode', 'agent-channels', `${values.agent}.json`),
|
|
154
|
-
});
|
|
50
|
+
const bridge = new AgentMailboxBridge({ adapter, client, agent: values.agent, platform: values.platform, fromName: `${values.platform}-${values.agent}`, log, statePath: statePath(values.platform) });
|
|
155
51
|
await bridge.start();
|
|
156
|
-
log('info', `${values.agent}:
|
|
157
|
-
|
|
158
|
-
const supercode = process.env.SUPERCODE_BIN || 'supercode';
|
|
159
|
-
const sc = (args) => JSON.parse(execFileSync(supercode, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
|
|
160
|
-
let boundTo = null, failedAs = null;
|
|
161
|
-
const lastLine = (error) => String(error.stderr || error.message).trim().split('\n').at(-1);
|
|
162
|
-
const warnOnce = (as, line) => { if (failedAs !== as) { failedAs = as; log('warn', line); } };
|
|
163
|
-
/** The agent's own seat takes its session at RH2's own-seat door (PUT …/members/:principalId/session, a Room
|
|
164
|
-
* reader's): the session alone changes, never the seat's roles or who may read and post to it, which stay a
|
|
165
|
-
* members.manage holder's. A refusal throws with its status. */
|
|
166
|
-
async function seatSession(session) {
|
|
167
|
-
const path = `/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(adapter.principalId())}/session`;
|
|
168
|
-
const res = await adapter.fetch(`${adapter.apiUrl()}/api/v3${path}`, { method: 'PUT', headers: adapter.headers(), body: JSON.stringify({ session }) });
|
|
169
|
-
if (!res.ok) throw new Error(`PUT ${path} -> HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
170
|
-
}
|
|
171
|
-
/** The participant's seat bound to the agent's current main session; a new main session rebinds it. The agent's record
|
|
172
|
-
* is read again on every tick, so a re-declared main is bound at the next one. A bind that fails is said once per main
|
|
173
|
-
* and reason (a main that is not in the team's catalog, or one no session ever ran as), never only at debug level,
|
|
174
|
-
* which this process does not print. */
|
|
175
|
-
async function bindMainSession() {
|
|
176
|
-
let main;
|
|
177
|
-
try { main = [sc(['agent', 'show', values.agent, '--json'])].flat().find((a) => a.name === values.agent)?.main_session; }
|
|
178
|
-
catch (error) { warnOnce(`agent show: ${lastLine(error)}`, `${values.agent}: its agent record was not read (${lastLine(error)}); its seat stays bound to ${boundTo ?? 'nothing'}`); return; }
|
|
179
|
-
const session = typeof main === 'string' ? main.split(':').pop() : null;
|
|
180
|
-
if (!session) { warnOnce('no main', `${values.agent}: its agent record names no main session; its seat stays bound to ${boundTo ?? 'nothing'}`); return; }
|
|
181
|
-
if (session === boundTo) return;
|
|
182
|
-
let bound;
|
|
183
|
-
try { bound = sc(['sessions', 'bind', session, '--room', roomId, '--agent', adapter.principalId(), '--rh2', adapter.apiUrl(), '--json']).data; }
|
|
184
|
-
catch (error) { warnOnce(`${session}: ${lastLine(error)}`, `${values.agent}: its seat is not bound to its main session ${main} (exit ${error.status ?? '?'}: ${lastLine(error)}); retried every 15 s with the record read again`); return; }
|
|
185
|
-
// The session by its reference (RH2 ADR 0050 decision 4); RH2 takes the registration that provides this agent's sessions.
|
|
186
|
-
await seatSession({ harness: bound.session.harness, reference: bound.session.reference });
|
|
187
|
-
failedAs = null;
|
|
188
|
-
boundTo = session;
|
|
189
|
-
log('info', `${values.agent}: its seat in the account Room carries its main session ${session}`);
|
|
190
|
-
}
|
|
191
|
-
// A refused seat is said once per reason and retried on the next tick, the first one too: it never ends the channel (a
|
|
192
|
-
// refusal at start would otherwise restart the service in a loop, with the mailbox bridge down each time).
|
|
193
|
-
const rebind = () => bindMainSession().catch((error) => warnOnce(`seat: ${error.message}`, `${values.agent}: its seat was refused its main session (${error.message}); retried every 15 s`));
|
|
194
|
-
await rebind();
|
|
195
|
-
const rebinding = setInterval(() => { void rebind(); }, 15_000);
|
|
196
|
-
const stop = async () => { clearInterval(rebinding); await bridge.stop(); await client.close?.(); process.exit(0); };
|
|
52
|
+
log('info', `${values.agent}: ${values.platform} is carried to its mailbox`);
|
|
53
|
+
const stop = async () => { await bridge.stop(); await adapter.close?.(); await client.close?.(); process.exit(0); };
|
|
197
54
|
process.on('SIGTERM', stop);
|
|
198
55
|
process.on('SIGINT', stop);
|
|
199
|
-
}
|
|
@@ -4,11 +4,10 @@ import { existsSync, openSync, closeSync, readFileSync, unlinkSync, renameSync }
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
|
-
import { activeAgentAssignments, localAgentConversation } from '../agents.mjs';
|
|
8
7
|
import { SERVICE_APPS, SERVICE_PROGRAM, connectionCall, connectionCanRun, connectionPath, generateModuleConfig, privateJson } from '../connection.mjs';
|
|
9
8
|
|
|
10
9
|
const {values}=parseArgs({options:{root:{type:'string'},connection:{type:'string'}}});
|
|
11
|
-
const path=connectionPath(values.root,values.connection),
|
|
10
|
+
const path=connectionPath(values.root,values.connection),lock=join(dirname(path),'runner.lock');
|
|
12
11
|
let record=JSON.parse(readFileSync(path,'utf8'));
|
|
13
12
|
record.root=values.root;
|
|
14
13
|
if(!connectionCanRun(record))process.exit(0);
|
|
@@ -40,7 +39,7 @@ function start(grant){
|
|
|
40
39
|
while((at=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,at);buffer=buffer.slice(at+1);let event;try{event=JSON.parse(line);}catch{continue;}
|
|
41
40
|
if(['read-error','read-failed','tick-failed','event-stream-failed','resync_required','access_revoked'].includes(event.event))issue(grant.name,event.code??event.reason??event.event);
|
|
42
41
|
if(['source-current','snapshot-read','snapshot-empty','published'].includes(event.event)&&event.current!==false)recover(grant.name);
|
|
43
|
-
if(
|
|
42
|
+
if(event.event==='card-pending')issue(`obligation.${grant.name}.${event.card}`,`${event.status?'HTTP '+event.status+' ':''}${event.code??event.event}${event.message?' — '+event.message:''}`.slice(0,300));
|
|
44
43
|
if(event.event==='projection-current')recover(`obligation.${grant.name}.${event.card}`);
|
|
45
44
|
}
|
|
46
45
|
if(buffer.length>65536)buffer='';
|
|
@@ -69,18 +68,6 @@ async function syncAlerts(){
|
|
|
69
68
|
}catch(error){log('connection-alert-pending',{module:grant.name,code:error.code??error.status??'unavailable'});}
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
|
-
function standing(){
|
|
73
|
-
const assignments=activeAgentAssignments(values.root);
|
|
74
|
-
const rows=(record.standingSelection??[]).map(selection=>{
|
|
75
|
-
const own=assignments.filter(row=>row.org===record.teams.team&&row.kind==='profile'&&row.target===selection.profile);
|
|
76
|
-
if(own.length!==1){issue(`standing.${selection.profile}`,own.length?'profile has multiple active agent owners':'profile has no declared conversation owner');return {...selection,reason:'conversation-owner-unknown'};}
|
|
77
|
-
const conversation=localAgentConversation(values.root,record.teams.team,own[0].agent_id);
|
|
78
|
-
if(!conversation){issue(`standing.${selection.profile}`,'the home has no conversation-session receipt for this profile');return {...selection,agent:own[0].agent_id,reason:'conversation-session-unknown'};}
|
|
79
|
-
recover(`standing.${selection.profile}`);return {...selection,agent:own[0].agent_id,session:conversation.session,startedAt:conversation.started_at,running:conversation.ended_at==null};
|
|
80
|
-
});
|
|
81
|
-
const target=join(folder,'standing.json');
|
|
82
|
-
if(!existsSync(target)||readFileSync(target,'utf8').trim()!==JSON.stringify(rows,null,2))privateJson(target,rows);
|
|
83
|
-
}
|
|
84
71
|
async function renewTeams(value){
|
|
85
72
|
if(!value.pendingRenewal&&value.credential.expires_at-Date.now()>7*86400000)return false;
|
|
86
73
|
if(value.pendingRenewal&&value.renewalRetryAt>Date.now())return false;
|
|
@@ -115,7 +102,7 @@ async function renew(){
|
|
|
115
102
|
}
|
|
116
103
|
for(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>void stop());
|
|
117
104
|
for(const grant of record.grants)start(grant);
|
|
118
|
-
let lastRenew=0
|
|
105
|
+
let lastRenew=0;
|
|
119
106
|
// Teams verifies the Workplace's tokens against its own JWKS (ADR 0070): nothing is relayed for them here.
|
|
120
107
|
while(!stopped){
|
|
121
108
|
try{
|
|
@@ -124,7 +111,6 @@ while(!stopped){
|
|
|
124
111
|
if(!installed.length){await stop();break;}
|
|
125
112
|
const pendingRenewalDue=[record.issuerCredential,...installed.map(grant=>grant.teams)].some(value=>value?.pendingRenewal&&!(value.renewalRetryAt>Date.now()));
|
|
126
113
|
if(pendingRenewalDue||Date.now()-lastRenew>3600000){await renew();lastRenew=Date.now();}
|
|
127
|
-
if(Date.now()-lastStanding>15000){standing();lastStanding=Date.now();}
|
|
128
114
|
recover('issuer');await syncAlerts();
|
|
129
115
|
}catch(error){issue('issuer',error.code??error.status??'connection-unavailable');await syncAlerts();}
|
|
130
116
|
await new Promise(done=>setTimeout(done,2000));
|
package/bin/orchestrator.mjs
CHANGED
|
@@ -2,24 +2,29 @@
|
|
|
2
2
|
// A reader such as `head` may close its pipe before the final flush. Finish the command and its document work;
|
|
3
3
|
// a closed output pipe is not an unhandled runtime error. Other stream failures retain their original error.
|
|
4
4
|
for (const stream of [process.stdout, process.stderr]) stream.on('error', error => { if (error.code !== 'EPIPE') throw error; });
|
|
5
|
+
if (process.argv[2] === 'workflow' && ['serve', 'daemon'].includes(process.argv[3])
|
|
6
|
+
&& !(typeof process.send === 'function' && Number(process.env.SUPERCODE_BOARD_GENERATION_OWNER) === process.ppid)) {
|
|
7
|
+
const { sourceServiceEntry } = await import('@volter/supercode-teams/source-service-entry');
|
|
8
|
+
await sourceServiceEntry(import.meta.url, 'sdk/orchestrator/bin/orchestrator.mjs');
|
|
9
|
+
}
|
|
5
10
|
// before anything loads node:sqlite: Node's notice that it is experimental is not this command's to print
|
|
6
11
|
try { await import('@volter/supercode-harness-sdk/runtime-warnings'); } catch { /* a harness SDK without it: the notice is printed */ }
|
|
7
12
|
// Check before handing off or importing board/runtime dependencies.
|
|
8
|
-
let
|
|
9
|
-
try { ({
|
|
13
|
+
let lockedProgram;
|
|
14
|
+
try { ({ lockedProgram } = await import('../source-lock.mjs')); }
|
|
10
15
|
catch (error) { process.stderr.write(`${error.message}\n`); process.exit(1); }
|
|
11
16
|
// Any command on a home whose record names another orchestrator is that orchestrator's (hand-off.mjs).
|
|
12
|
-
{
|
|
17
|
+
if (process.argv[2] !== 'workflow' && !(typeof process.send === 'function' && Number(process.env.SUPERCODE_BOARD_GENERATION_OWNER) === process.ppid)) {
|
|
13
18
|
const { handOff, homeOfArgs } = await import('../hand-off.mjs');
|
|
14
19
|
let handed;
|
|
15
20
|
try { handed = handOff(homeOfArgs(process.argv.slice(2)), process.argv.slice(2)); }
|
|
16
21
|
catch (error) { process.stderr.write(`supercode ${process.argv[2] ?? ''}: ${error.message}\n`); process.exit(1); }
|
|
17
22
|
if (handed !== null) process.exit(await handed);
|
|
18
23
|
}
|
|
19
|
-
// Hand-off may
|
|
24
|
+
// Hand-off may select the home's SUPERCODE_BIN. Verify its exact Node source entry before any child.
|
|
20
25
|
try {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
26
|
+
const pinnedProgram = lockedProgram();
|
|
27
|
+
if (pinnedProgram) process.env.SUPERCODE_BIN = pinnedProgram;
|
|
23
28
|
} catch (error) { process.stderr.write(`${error.message}\n`); process.exit(1); }
|
|
24
29
|
// Board commands do not load daemon transports, platform adapters or the MCP server.
|
|
25
30
|
if (process.argv[2] === 'workflow') {
|
|
@@ -40,7 +45,7 @@ if (process.argv[2] === 'workflow') {
|
|
|
40
45
|
const {parseArgs}=await import('node:util');
|
|
41
46
|
const {connectWorkplace}=await import('../connection.mjs');
|
|
42
47
|
const {SERVICE_REGISTRATIONS}=await import('../connection.mjs');
|
|
43
|
-
const options=Object.fromEntries(['root','ticket-file','modules-root','source','context','teams-token-file','teams-url','team','
|
|
48
|
+
const options=Object.fromEntries(['root','ticket-file','modules-root','source','context','teams-token-file','teams-url','team','alert-role','webhook-url','webhook-listen'].map(key=>[key,{type:'string'}]));
|
|
44
49
|
options['no-start']={type:'boolean'};options.registrations={type:'boolean'};
|
|
45
50
|
try{
|
|
46
51
|
const {values}=parseArgs({args:process.argv.slice(3),options});
|
package/bin/runtime.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `supercode orchestrator start` spawns the daemon form; the daemon itself
|
|
8
8
|
// writes the lease (`<home>/orchestrator.lock`) naming this process and
|
|
9
9
|
// removes it on the way out, so a service-managed daemon reports the same
|
|
10
|
-
// lease a foreground one does.
|
|
10
|
+
// lease a foreground one does. Public stop uses the incarnation-bound control door.
|
|
11
11
|
// A lock left behind by a process that is gone is the mark of a daemon that
|
|
12
12
|
// died: this one dispatches `restart` (§4.7) before it accepts anything.
|
|
13
13
|
//
|
|
@@ -53,12 +53,14 @@
|
|
|
53
53
|
// Everything it prints is one JSON object per line on stdout.
|
|
54
54
|
|
|
55
55
|
import { SenderIdentityRecord } from '@volter/teams/sender-identity';
|
|
56
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
56
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, realpathSync } from 'node:fs';
|
|
57
57
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
58
|
-
import { spawn
|
|
58
|
+
import { spawn } from 'node:child_process';
|
|
59
|
+
import { spawnSupercodeSync } from '@volter/supercode-harness-sdk/supercode-program';
|
|
59
60
|
import { fileURLToPath } from 'node:url';
|
|
60
61
|
import { homedir } from 'node:os';
|
|
61
62
|
import { LOCK_FILE, leaseIsLive, readLease, thisHost } from '../lease.mjs';
|
|
63
|
+
import { newDaemonIncarnation, clearDaemonLease, startDaemonControl } from '../daemon-control.mjs';
|
|
62
64
|
import { socketPathFor } from '../mcp/server.mjs';
|
|
63
65
|
import { GATEWAY_KIND, holdGatewayLock } from '../hermes-locks.mjs';
|
|
64
66
|
import { adoptHome } from '../hermes-home.mjs';
|
|
@@ -70,7 +72,7 @@ import {
|
|
|
70
72
|
TICK_MS, accessPolicy, applyOperator, performRemoveProfileDir, OPERATOR_OPS,
|
|
71
73
|
} from '../index.mjs';
|
|
72
74
|
import * as events from '../events.mjs';
|
|
73
|
-
import { appView, installAgent, installApp, installedAgentRecord, installedApps, removeApp, rh2AgentEnv, rh2Program, startInstalledModules } from '../installed-apps.mjs';
|
|
75
|
+
import { appView, installAgent, installApp, installedAgentRecord, installedAgentRecords, installedApps, removeApp, rh2AgentEnv, rh2Program, startInstalledModules } from '../installed-apps.mjs';
|
|
74
76
|
import { httpJobExecutor } from '../hosted.mjs';
|
|
75
77
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
76
78
|
|
|
@@ -363,9 +365,9 @@ function buildAdapters(state, vault = new Map(), log = () => {}, root = null) {
|
|
|
363
365
|
// installed for. Uninstalling on the other side revokes the token and the channel stops.
|
|
364
366
|
for (const app of root ? installedApps(root) : []) {
|
|
365
367
|
if (app.app !== 'room-chat') continue;
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
if (mailAgentNamed(app.profile)) { report.push({ platform: 'rh2', profile: app.profile, app: app.name, state: '
|
|
368
|
+
// A mail agent (its name as the profile) is in a Room as its own principal, brought in by the Workplace's join command,
|
|
369
|
+
// which delivers the Room's lines to its session (runhuman-2 ADR 0063): never a worker's turns here.
|
|
370
|
+
if (mailAgentNamed(app.profile)) { report.push({ platform: 'rh2', profile: app.profile, app: app.name, state: 'unsupported (a mail agent is in its Rooms as its own principal)' }); continue; }
|
|
369
371
|
if (!state.profiles[app.profile]) { report.push({ platform: 'rh2', profile: app.profile, app: app.name, state: 'unsupported (its profile does not exist)' }); continue; }
|
|
370
372
|
if (adapters.rh2) { report.push({ platform: 'rh2', profile: app.profile, app: app.name, state: 'already owned by another profile' }); continue; }
|
|
371
373
|
adapters.rh2 = new Rh2Adapter({ platform: 'rh2', profile: app.profile, log, vault, channel: { enabled: true, extra: { api_url: app.origin, organization: app.organizationId, principal_id: app.principalId, token: app.token } } });
|
|
@@ -376,7 +378,7 @@ function buildAdapters(state, vault = new Map(), log = () => {}, root = null) {
|
|
|
376
378
|
|
|
377
379
|
/** Whether `name` is a declared mail agent on this machine (`supercode agent show`). */
|
|
378
380
|
function mailAgentNamed(name) {
|
|
379
|
-
const shown =
|
|
381
|
+
const shown = spawnSupercodeSync(process.env.SUPERCODE_BIN || 'supercode', ['agent', 'show', name, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
380
382
|
try { return shown.status === 0 && [JSON.parse(shown.stdout)].flat().some((agent) => agent?.name === name); } catch { return false; }
|
|
381
383
|
}
|
|
382
384
|
|
|
@@ -405,11 +407,7 @@ function writeLock(root, lease) {
|
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
/** Only ever removes the record this process wrote. */
|
|
408
|
-
|
|
409
|
-
const held = readLock(root);
|
|
410
|
-
if (held && held.pid !== process.pid) return;
|
|
411
|
-
try { unlinkSync(join(root, LOCK_FILE)); } catch { /* already gone */ }
|
|
412
|
-
}
|
|
410
|
+
const clearLock = clearDaemonLease;
|
|
413
411
|
|
|
414
412
|
// ---------------------------------------------------------------- access verbs
|
|
415
413
|
|
|
@@ -480,7 +478,10 @@ async function appsMain(argv) {
|
|
|
480
478
|
process.stderr.write('apps mcp rh2-agent --agent <its Teams id> [--root R]\n');
|
|
481
479
|
process.exit(2);
|
|
482
480
|
}
|
|
483
|
-
const
|
|
481
|
+
const home = resolve(opts.root ?? defaultRoot());
|
|
482
|
+
// its first Workplace: the one record, else the one installed by hand, else the first by origin; the rest are read
|
|
483
|
+
// per call from the home (RH2_AGENT_HOMES), so a call on another Workplace is made on the agent's record there
|
|
484
|
+
const record = installedAgentRecord(home, opts.agent) ?? installedAgentRecords(home, opts.agent)[0] ?? null;
|
|
484
485
|
if (!record) {
|
|
485
486
|
process.stderr.write(`no single rh2-agent record is installed for ${opts.agent}: install it with apps install rh2-agent --agent ${opts.agent}\n`);
|
|
486
487
|
process.exit(1);
|
|
@@ -490,7 +491,7 @@ async function appsMain(argv) {
|
|
|
490
491
|
process.stderr.write(`apps mcp: rh2 is not on PATH (searched ${process.env.PATH || 'an empty PATH'})\n`);
|
|
491
492
|
process.exit(127);
|
|
492
493
|
}
|
|
493
|
-
const child = spawn(program, ['mcp'], { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record) } });
|
|
494
|
+
const child = spawn(program, ['mcp'], { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record, home) } });
|
|
494
495
|
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
495
496
|
child.on('error', (error) => { process.stderr.write(`apps mcp: ${error.message}\n`); process.exit(127); });
|
|
496
497
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
@@ -710,7 +711,8 @@ async function main() {
|
|
|
710
711
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
711
712
|
return;
|
|
712
713
|
}
|
|
713
|
-
|
|
714
|
+
let root = resolve(args.root ?? defaultRoot());
|
|
715
|
+
let daemonLease = null;
|
|
714
716
|
let crashed = false;
|
|
715
717
|
let gatewayLock = null;
|
|
716
718
|
if (!args.once) {
|
|
@@ -729,17 +731,19 @@ async function main() {
|
|
|
729
731
|
}
|
|
730
732
|
// Who serves the home now (`orchestrator.lock`), for `supercode orchestrator status` and the operator verbs' live
|
|
731
733
|
// door. A record left behind is a predecessor that never stopped cleanly: this start replays for it (§4.7).
|
|
734
|
+
root = realpathSync(root);
|
|
732
735
|
const previous = readLock(root);
|
|
733
736
|
crashed = Boolean(previous && previous.pid !== process.pid);
|
|
734
737
|
if (previous) unlinkSync(join(root, LOCK_FILE));
|
|
735
|
-
|
|
738
|
+
daemonLease = Object.freeze({ pid: process.pid, started_at: new Date().toISOString(), root, ...thisHost(), incarnation: newDaemonIncarnation(), launch_request: process.env.SUPERCODE_ORCHESTRATOR_START_REQUEST ?? null, executor_url: args.executorUrl ?? null });
|
|
739
|
+
writeLock(root, daemonLease);
|
|
736
740
|
try {
|
|
737
741
|
// the home in both forms before it is loaded: a Hermes home is adopted as it stands
|
|
738
742
|
for (const change of adoptHome(root)) say('log', { level: 'info', message: `home: ${change}` });
|
|
739
743
|
} catch (error) {
|
|
740
744
|
say('error', { root, message: error.message });
|
|
741
|
-
|
|
742
|
-
|
|
745
|
+
clearLock(root, daemonLease);
|
|
746
|
+
await gatewayLock?.release();
|
|
743
747
|
process.exit(1);
|
|
744
748
|
}
|
|
745
749
|
}
|
|
@@ -763,9 +767,9 @@ async function main() {
|
|
|
763
767
|
profileLocks.push(await holdGatewayLock(profile.dir));
|
|
764
768
|
} catch (error) {
|
|
765
769
|
say('error', { root, message: error.message });
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
770
|
+
clearLock(root, daemonLease);
|
|
771
|
+
for (const lock of profileLocks) await lock.release();
|
|
772
|
+
await gatewayLock.release();
|
|
769
773
|
process.exit(1);
|
|
770
774
|
}
|
|
771
775
|
}
|
|
@@ -802,12 +806,53 @@ async function main() {
|
|
|
802
806
|
}
|
|
803
807
|
|
|
804
808
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
809
|
+
let keepalive = null, control = null, stopped = null, finishing = null, starting = null;
|
|
810
|
+
const stop = (reason) => stopped ??= (async () => {
|
|
811
|
+
await starting?.catch(() => {});
|
|
812
|
+
clearInterval(keepalive);
|
|
813
|
+
say('stopping', { root, pid: process.pid, reason, incarnation: daemonLease.incarnation });
|
|
814
|
+
const errors = [];
|
|
815
|
+
const joinOwned = async (name, work) => {
|
|
816
|
+
try { await work(); }
|
|
817
|
+
catch (error) { errors.push(`${name}: ${String(error.message ?? error)}`); }
|
|
818
|
+
};
|
|
819
|
+
// Join each owner even if another failed. Never erase a replacement lease.
|
|
820
|
+
await joinOwned('orchestrator', () => orchestrator.stop());
|
|
821
|
+
await joinOwned('client', () => client.close?.());
|
|
822
|
+
await joinOwned('World', () => closeWorld());
|
|
823
|
+
for (const lock of profileLocks) await joinOwned('profile lock', () => lock.release());
|
|
824
|
+
if (!errors.length) await joinOwned('lease', () => clearLock(root, daemonLease));
|
|
825
|
+
await joinOwned('gateway lock', () => gatewayLock?.release());
|
|
826
|
+
const receipt = { version: 1, daemon: daemonLease, status: errors.length ? 'failed' : 'stopped', errors };
|
|
827
|
+
say(receipt.status, receipt);
|
|
828
|
+
return receipt;
|
|
829
|
+
})();
|
|
830
|
+
const finish = () => finishing ??= (async () => {
|
|
831
|
+
const receipt = await stop('signal');
|
|
832
|
+
try { await control?.close(); }
|
|
833
|
+
catch (error) { say('error', { root, message: `control close: ${error.message}` }); process.exitCode = 1; }
|
|
834
|
+
await exitAfterFlush(receipt.status === 'stopped' && !process.exitCode ? 0 : 1);
|
|
835
|
+
})();
|
|
836
|
+
process.on('SIGTERM', () => { void stop('SIGTERM'); void finish(); });
|
|
837
|
+
process.on('SIGINT', () => { void stop('SIGINT'); void finish(); });
|
|
838
|
+
try {
|
|
839
|
+
starting = (async () => {
|
|
840
|
+
await orchestrator.start();
|
|
841
|
+
// A daemon that came BACK replays pending obligations, re-attaches or
|
|
842
|
+
// interrupts the bindings that held a lease, and clears the stale ones
|
|
843
|
+
// before it accepts anything new (§4.7). A clean stop removes its own lock,
|
|
844
|
+
// so an ordinary start finds none and replays nothing.
|
|
845
|
+
if (!stopped && crashed) await orchestrator.dispatch(events.restart(new Date().toISOString()));
|
|
846
|
+
if (!stopped) control = await startDaemonControl({ root, lease: daemonLease, stop, complete: () => { void finish(); } });
|
|
847
|
+
})();
|
|
848
|
+
await starting;
|
|
849
|
+
if (stopped) { await finish(); return; }
|
|
850
|
+
} catch (error) {
|
|
851
|
+
say('error', { root, message: String(error.message ?? error) });
|
|
852
|
+
process.exitCode = 1;
|
|
853
|
+
await finish();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
811
856
|
say('ready', {
|
|
812
857
|
root,
|
|
813
858
|
pid: process.pid,
|
|
@@ -822,29 +867,7 @@ async function main() {
|
|
|
822
867
|
// The loop's own tick timer is `unref`'d, and the webhook listener may bind
|
|
823
868
|
// nothing a profile uses; this timer is what keeps the daemon alive on
|
|
824
869
|
// purpose, so "up" always means a process that will still be here next tick.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
let stopping = false;
|
|
828
|
-
const stop = async (signal) => {
|
|
829
|
-
if (stopping) return;
|
|
830
|
-
stopping = true;
|
|
831
|
-
clearInterval(keepalive);
|
|
832
|
-
say('stopping', { root, pid: process.pid, signal });
|
|
833
|
-
try {
|
|
834
|
-
await orchestrator.stop();
|
|
835
|
-
await client.close?.();
|
|
836
|
-
await closeWorld();
|
|
837
|
-
} catch (error) {
|
|
838
|
-
say('log', { level: 'error', message: `stop: ${String(error.message ?? error)}` });
|
|
839
|
-
}
|
|
840
|
-
for (const lock of profileLocks) lock.release();
|
|
841
|
-
gatewayLock?.release();
|
|
842
|
-
clearLock(root);
|
|
843
|
-
say('stopped', { root, pid: process.pid });
|
|
844
|
-
process.exit(0);
|
|
845
|
-
};
|
|
846
|
-
process.on('SIGTERM', () => void stop('SIGTERM'));
|
|
847
|
-
process.on('SIGINT', () => void stop('SIGINT'));
|
|
870
|
+
keepalive = setInterval(() => {}, TICK_MS);
|
|
848
871
|
}
|
|
849
872
|
|
|
850
873
|
await main();
|
package/board/call.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
17
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
18
18
|
import { withCredentialGuard } from './credential-guard.mjs';
|
|
19
|
+
import { generationWork } from './generation.mjs';
|
|
19
20
|
|
|
20
21
|
const calls = new AsyncLocalStorage();
|
|
21
22
|
// what a call learns as it runs, and the workflow it read (pinned: a reload while it runs does not change its basis)
|
|
@@ -35,7 +36,7 @@ export function within({ caller = null, facts = {}, cwd, stdin = null, out, err,
|
|
|
35
36
|
/** Run `fn` as a unit of the process's own work (a round, a delivery pass): the process's identity and output, with
|
|
36
37
|
* what it learns and the workflow it read kept to itself. */
|
|
37
38
|
export function ownCall(fn) {
|
|
38
|
-
return withCredentialGuard(() => calls.run({ kind: 'own', caller: null, facts: process.env, ...fresh(), get cwd() { return process.cwd(); }, stdin: null, out: own.out, err: own.err }, fn));
|
|
39
|
+
return generationWork(() => withCredentialGuard(() => calls.run({ kind: 'own', caller: null, facts: process.env, ...fresh(), get cwd() { return process.cwd(); }, stdin: null, out: own.out, err: own.err }, fn)));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/** The caller as its address names it, or null. */
|