@volter/supercode-orchestrator 0.5.215 → 0.5.217
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 +147 -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 +294 -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 +9 -6
- package/reducer.mjs +31 -6
- package/source-lock.mjs +15 -24
- package/webhook.mjs +65 -12
package/source-lock.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The orchestrator shares the CLI/Teams source lock; it declares no second binding.
|
|
2
3
|
import { existsSync, realpathSync } from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { enforceSourceLock, sourceRoot, SourceLockError } from '@volter/supercode-teams/source-lock';
|
|
6
|
+
import { supercodeInvocation } from '@volter/supercode-harness-sdk/supercode-program';
|
|
6
7
|
|
|
7
8
|
const lock = enforceSourceLock(import.meta.url, 'the orchestrator');
|
|
8
9
|
if (lock) {
|
|
@@ -10,30 +11,20 @@ if (lock) {
|
|
|
10
11
|
enforceSourceLock(import.meta.resolve('@volter/supercode-teams/source-lock'), 'the orchestrator Teams package');
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/** The locked tree's release native, proved against the same lock before any codec door opens. */
|
|
16
|
-
export function lockedNative() {
|
|
14
|
+
/** The exact pinned Node front door; no native build receipt is inferred. */
|
|
15
|
+
export function lockedProgram() {
|
|
17
16
|
const lock = enforceSourceLock(import.meta.url, 'the orchestrator');
|
|
18
17
|
if (!lock) return null;
|
|
18
|
+
if (lock.binding !== 'node') throw new SourceLockError("supercode source lock: this declaration binds a native build; run the intended tree's Node front door with `source pin` to select its Node binding");
|
|
19
19
|
const root = sourceRoot(import.meta.url);
|
|
20
|
-
const
|
|
21
|
-
if (!existsSync(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (realpathSync(
|
|
26
|
-
throw new SourceLockError(`supercode source lock: SUPERCODE_BIN must name the pinned
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const result = spawnSync(binary, ['source', 'show', '--json'], { encoding: 'utf8', timeout: 5000 });
|
|
30
|
-
if (result.error || result.status !== 0) throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} cannot be verified: ${result.error?.message || result.stderr}`);
|
|
31
|
-
const receipt = JSON.parse(result.stdout);
|
|
32
|
-
// The native is judged by its binding alone: a retired copy that came back (drift) is said, and never stops the board.
|
|
33
|
-
const binding = receipt.binding ?? receipt.verdict;
|
|
34
|
-
if (binding !== 'matches' || receipt.native.version !== lock.supercode || receipt.native.commit !== lock.commit || receipt.native.dirty) {
|
|
35
|
-
throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} does not match ${lock.path}: ${binding}`);
|
|
20
|
+
const entry = resolve(root, 'packaging/npm/bin/supercode.js');
|
|
21
|
+
if (!existsSync(entry)) throw new SourceLockError(`supercode source lock: the orchestrator needs the pinned tree's Node front door (${entry})`);
|
|
22
|
+
enforceSourceLock(entry, 'the orchestrator supercode front door');
|
|
23
|
+
if (process.env.SUPERCODE_BIN) {
|
|
24
|
+
const selected = supercodeInvocation(process.env.SUPERCODE_BIN, [], process.env);
|
|
25
|
+
if (!selected.node || !existsSync(selected.arguments[0]) || realpathSync(selected.arguments[0]) !== realpathSync(entry))
|
|
26
|
+
throw new SourceLockError(`supercode source lock: SUPERCODE_BIN must name the pinned Node front door (${entry}) or its known Node shim`);
|
|
27
|
+
enforceSourceLock(selected.arguments[0], 'the orchestrator SUPERCODE_BIN');
|
|
36
28
|
}
|
|
37
|
-
|
|
38
|
-
return binary;
|
|
29
|
+
return entry;
|
|
39
30
|
}
|
package/webhook.mjs
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
// and `node:http` at call time instead.
|
|
11
11
|
|
|
12
12
|
import { surfaceKeyString, sessionReset } from './ir.mjs';
|
|
13
|
-
import { endBinding, ensureRuntime, expired, leaseId, liveBinding, newObligation,
|
|
13
|
+
import { endBinding, ensureRuntime, expired, leaseId, liveBinding, newObligation, startWorker } from './reducer.mjs';
|
|
14
14
|
import { budgetRefusal } from './conversations.mjs';
|
|
15
15
|
import { compactStamp, resolveTarget } from './cron.mjs';
|
|
16
16
|
import * as events from './events.mjs';
|
|
17
17
|
|
|
18
|
+
/** An intentional pre-acknowledgement refusal, not a physical cleanup failure. */
|
|
19
|
+
export class WebhookRefusal extends Error {}
|
|
20
|
+
|
|
18
21
|
/** The synthetic surface a subscription's turns run on (§4.4). */
|
|
19
22
|
export function webhookKey(name) {
|
|
20
23
|
return { platform: 'webhook', kind: 'dm', chat_id: name };
|
|
@@ -114,7 +117,10 @@ export function handleWebhook(state, ev, effects) {
|
|
|
114
117
|
// a redelivery (GitHub's "Redeliver", a sender's retry) is the same event:
|
|
115
118
|
// it runs once. The memory is runtime state (§2.8), as Hermes's is: a
|
|
116
119
|
// restart forgets it.
|
|
117
|
-
const
|
|
120
|
+
const runtime = ensureRuntime(state);
|
|
121
|
+
runtime.webhook_deliveries ??= {};
|
|
122
|
+
runtime.webhook_deliveries[ev.name] ??= [];
|
|
123
|
+
const seen = runtime.webhook_deliveries[ev.name];
|
|
118
124
|
if (seen.includes(ev.delivery_id)) {
|
|
119
125
|
effects.push({ type: 'log', level: 'info', message: `webhook ${ev.name}: delivery ${ev.delivery_id} already handled, dropped` });
|
|
120
126
|
return;
|
|
@@ -145,6 +151,7 @@ export function handleWebhook(state, ev, effects) {
|
|
|
145
151
|
|
|
146
152
|
const key = webhookKey(ev.name);
|
|
147
153
|
const keyString = surfaceKeyString(key);
|
|
154
|
+
if (ev.admission_id) effects.push({ type: 'webhook_input', admission_id: ev.admission_id, profile: profile.name, keyString });
|
|
148
155
|
let b = liveBinding(profile, keyString);
|
|
149
156
|
if (b) {
|
|
150
157
|
const reason = expired(b, sessionReset(profile), now);
|
|
@@ -153,7 +160,11 @@ export function handleWebhook(state, ev, effects) {
|
|
|
153
160
|
if (!b) {
|
|
154
161
|
// G3: an agent over its budget starts nothing; the refusal is told, never a pause
|
|
155
162
|
const refused = budgetRefusal(state, profile, profile.name, now);
|
|
156
|
-
if (refused) {
|
|
163
|
+
if (refused) {
|
|
164
|
+
effects.push({ type: 'budget_refused', profile: profile.name, agent: profile.name, reason: refused });
|
|
165
|
+
if (ev.admission_id) effects.push({ type: 'webhook_input', admission_id: ev.admission_id, profile: profile.name, keyString, refusal: refused });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
157
168
|
b = {
|
|
158
169
|
key, agent: profile.name,
|
|
159
170
|
worker: { harness: profile.worker.harness, session_id: `webhook_${ev.name}_${compactStamp(now)}`, locator: null },
|
|
@@ -161,7 +172,6 @@ export function handleWebhook(state, ev, effects) {
|
|
|
161
172
|
recurrence: null, residue: { webhook: ev.name },
|
|
162
173
|
};
|
|
163
174
|
profile.bindings[keyString] = b;
|
|
164
|
-
effects.push({ type: 'runtime_start', profile: profile.name, keyString, worker: profile.worker, prompt: null, agent: profile.name });
|
|
165
175
|
}
|
|
166
176
|
b.residue.webhook = ev.name;
|
|
167
177
|
b.last_activity_at = now;
|
|
@@ -169,10 +179,14 @@ export function handleWebhook(state, ev, effects) {
|
|
|
169
179
|
const text = renderTemplate(sub.prompt_template, ev.payload);
|
|
170
180
|
const rt = ensureRuntime(state);
|
|
171
181
|
const lid = leaseId(profile.name, keyString);
|
|
172
|
-
if (rt.
|
|
182
|
+
if (!rt.leases[lid] && !rt.starting[lid] && !startWorker(state, profile, keyString, effects, now)) {
|
|
183
|
+
if (ev.admission_id) effects.push({ type: 'webhook_input', admission_id: ev.admission_id, profile: profile.name, keyString, refusal: 'worker start refused; retry after its budget permits' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (rt.turns[lid] || !rt.leases[lid]) { rt.queues[lid] ??= []; rt.queues[lid].push({ text, at: now, ...(ev.admission_id ? { webhook_input_id: ev.admission_id } : {}) }); }
|
|
173
187
|
else {
|
|
174
|
-
rt.turns[lid] = { started_at: now, buffer: '' };
|
|
175
|
-
effects.push({ type: 'send_input', profile: profile.name, keyString, text });
|
|
188
|
+
rt.turns[lid] = { started_at: now, buffer: '', ...(ev.admission_id ? { webhook_input_id: ev.admission_id } : {}) };
|
|
189
|
+
effects.push({ type: 'send_input', profile: profile.name, keyString, text, ...(ev.admission_id ? { webhook_input_id: ev.admission_id } : {}) });
|
|
176
190
|
}
|
|
177
191
|
effects.push({ type: 'save' });
|
|
178
192
|
}
|
|
@@ -214,7 +228,10 @@ const MAX_BODY = 1024 * 1024;
|
|
|
214
228
|
*/
|
|
215
229
|
export async function startWebhookListener({ state, vault, port = 0, host = '127.0.0.1', dispatch, clock, log = () => {} }) {
|
|
216
230
|
const { createServer } = await import('node:http');
|
|
231
|
+
const sockets = new Set(), admitted = new Map(), tasks = new Set(), failures = [];
|
|
232
|
+
let closing = false, closePromise;
|
|
217
233
|
const server = createServer((req, res) => {
|
|
234
|
+
if (closing) { req.destroy(); return; }
|
|
218
235
|
const match = /^\/webhooks\/([^/?#]+)/.exec(req.url ?? '');
|
|
219
236
|
if (req.method !== 'POST' || !match) { res.writeHead(404, { 'content-type': 'application/json' }); res.end('{"error":"not found"}'); return; }
|
|
220
237
|
let name;
|
|
@@ -229,10 +246,36 @@ export async function startWebhookListener({ state, vault, port = 0, host = '127
|
|
|
229
246
|
chunks.push(c);
|
|
230
247
|
});
|
|
231
248
|
req.on('end', () => {
|
|
249
|
+
if (closing) return;
|
|
232
250
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
233
|
-
|
|
251
|
+
const socket = req.socket;
|
|
252
|
+
admitted.set(socket, (admitted.get(socket) ?? 0) + 1);
|
|
253
|
+
const task = handleRequest({ state, vault, name, body, headers: req.headers, dispatch, clock, log, res });
|
|
254
|
+
tasks.add(task);
|
|
255
|
+
const finished = () => {
|
|
256
|
+
tasks.delete(task);
|
|
257
|
+
const count = admitted.get(socket) - 1;
|
|
258
|
+
if (count) admitted.set(socket, count); else admitted.delete(socket);
|
|
259
|
+
if (closing) socket.destroy();
|
|
260
|
+
};
|
|
261
|
+
void task.then(finished, (error) => {
|
|
262
|
+
if (!(error instanceof WebhookRefusal)) failures.push(error);
|
|
263
|
+
// No positive acknowledgement may precede failed verification or loop
|
|
264
|
+
// processing. Retain the failure for close as well as telling the sender.
|
|
265
|
+
if (!res.headersSent && !res.destroyed) {
|
|
266
|
+
res.writeHead(503, { 'content-type': 'application/json', 'retry-after': '1' });
|
|
267
|
+
res.end('{"ok":false,"error":"webhook processing failed; retry"}');
|
|
268
|
+
}
|
|
269
|
+
finished();
|
|
270
|
+
log('warn', `webhook ${name}: ${String(error?.message ?? error)}`);
|
|
271
|
+
});
|
|
234
272
|
});
|
|
235
|
-
req.on('error', (error) => log('warn', `webhook ${name}: ${error.message}`));
|
|
273
|
+
req.on('error', (error) => { if (!closing) log('warn', `webhook ${name}: ${error.message}`); });
|
|
274
|
+
});
|
|
275
|
+
server.on('connection', (socket) => {
|
|
276
|
+
sockets.add(socket);
|
|
277
|
+
socket.once('close', () => sockets.delete(socket));
|
|
278
|
+
if (closing) socket.destroy();
|
|
236
279
|
});
|
|
237
280
|
await new Promise((resolve, reject) => {
|
|
238
281
|
server.once('error', reject);
|
|
@@ -242,7 +285,17 @@ export async function startWebhookListener({ state, vault, port = 0, host = '127
|
|
|
242
285
|
return {
|
|
243
286
|
server,
|
|
244
287
|
port: server.address().port,
|
|
245
|
-
close: () =>
|
|
288
|
+
close: () => closePromise ??= (async () => {
|
|
289
|
+
closing = true;
|
|
290
|
+
const closed = new Promise(resolve => server.close(error => { if (error) failures.push(error); resolve(); }));
|
|
291
|
+
// A partial body is passive ingress, not admitted loop work. It cannot
|
|
292
|
+
// hold later runtime/adapter cleanup behind HTTP's request timeout.
|
|
293
|
+
for (const socket of sockets) if (!admitted.has(socket)) socket.destroy();
|
|
294
|
+
await Promise.allSettled([...tasks]);
|
|
295
|
+
for (const socket of sockets) socket.destroy();
|
|
296
|
+
await closed;
|
|
297
|
+
if (failures.length) throw new AggregateError(failures, failures.map(error => String(error?.message ?? error)).join('; '));
|
|
298
|
+
})(),
|
|
246
299
|
};
|
|
247
300
|
}
|
|
248
301
|
|
|
@@ -259,9 +312,9 @@ async function handleRequest({ state, vault, name, body, headers, dispatch, cloc
|
|
|
259
312
|
let payload;
|
|
260
313
|
try { payload = JSON.parse(body); } catch { payload = { body }; }
|
|
261
314
|
const at = clock();
|
|
262
|
-
res.writeHead(verified ? 202 : 401, { 'content-type': 'application/json' });
|
|
263
|
-
res.end(JSON.stringify({ ok: verified, name }));
|
|
264
315
|
const event = firstHeader(headers, EVENT_HEADERS) ?? eventType(payload);
|
|
265
316
|
const delivery_id = firstHeader(headers, DELIVERY_HEADERS);
|
|
266
317
|
await dispatch(events.webhook({ name, payload, verified, at, event, delivery_id }));
|
|
318
|
+
res.writeHead(verified ? 202 : 401, { 'content-type': 'application/json' });
|
|
319
|
+
res.end(JSON.stringify({ ok: verified, name }));
|
|
267
320
|
}
|