@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/loop.mjs
CHANGED
|
@@ -10,14 +10,14 @@ import { parseSessionWire, stringifySessionWire } from '@volter/supercode-teams/
|
|
|
10
10
|
import { step, leaseId } from './reducer.mjs';
|
|
11
11
|
import * as events from './events.mjs';
|
|
12
12
|
import { read, save, retainWorld, releaseWorld, refreshVault } from './orchestration.mjs';
|
|
13
|
-
import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, workerCwd, declareWorkerFolder, declareWorkerHome, approvalPolicy,
|
|
13
|
+
import { INSTRUCTION_ARGUMENTS, NAME_ARGUMENTS, activationEnv, activationCwd, workerCwd, declareWorkerFolder, declareWorkerHome, approvalPolicy, approvalsReviewer, mainApprovalArguments, modeArguments, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
|
|
14
14
|
import { workerRoute, codexPaneRoute } from './worker-route.mjs';
|
|
15
15
|
import { projectCodexPaneResume } from './codex-pane-resume.mjs';
|
|
16
16
|
import { hermesAccess, hermesApproval, untrustedServerVerdict, hermesToolEnv, memorySnapshot, mirrorSession, mirrorState, recordIncident, sealMirror } from './hermes-cli.mjs';
|
|
17
17
|
import { hermesSessionKey, homeChannel } from './ir.mjs';
|
|
18
18
|
import { surfaceKeyString } from './ir.mjs';
|
|
19
19
|
import { BREAKER_FAILURES, BREAKER_MINUTES, MAX_ATTEMPTS, historyRetention, jobGate } from './cron.mjs';
|
|
20
|
-
import { startWebhookListener } from './webhook.mjs';
|
|
20
|
+
import { startWebhookListener, WebhookRefusal, findSubscription } from './webhook.mjs';
|
|
21
21
|
import { startBridge } from './mcp/bridge.mjs';
|
|
22
22
|
import { socketPathFor } from './mcp/server.mjs';
|
|
23
23
|
import { mountOrchestratorMcp } from './mcp/mount.mjs';
|
|
@@ -29,7 +29,9 @@ import { agentScope,localProfileAgent,localAgentRecord,localAgentSessions,localF
|
|
|
29
29
|
import { materializeAgentHome } from './agent-home.mjs';
|
|
30
30
|
import { knowLocalMachine } from './board/dispatch.mjs';
|
|
31
31
|
import { execFile } from 'node:child_process';
|
|
32
|
+
import { supercodeInvocation } from '@volter/supercode-harness-sdk/supercode-program';
|
|
32
33
|
import { promisify } from 'node:util';
|
|
34
|
+
import { randomUUID } from 'node:crypto';
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Where a worker keeps its transcripts inside its config home (`<profile>/<harness>`), as discovery and
|
|
@@ -80,6 +82,9 @@ export class Orchestrator {
|
|
|
80
82
|
this.log = log;
|
|
81
83
|
this.runtimes = new Map(); // leaseId -> ManagedRuntime
|
|
82
84
|
this.queue = Promise.resolve();
|
|
85
|
+
this.internalDispatches = new Set();
|
|
86
|
+
this.internalFailures = [];
|
|
87
|
+
this.webhookAdmissions = new Map();
|
|
83
88
|
this.effectLog = [];
|
|
84
89
|
this.support = null; // memoized `supportReport()`: the registry does not move under a run
|
|
85
90
|
}
|
|
@@ -90,7 +95,9 @@ export class Orchestrator {
|
|
|
90
95
|
for (const [platform, adapter] of Object.entries(this.adapters)) await this.connectAdapter(platform, adapter);
|
|
91
96
|
this.webhooks = await startWebhookListener({
|
|
92
97
|
state: this.state, vault: this.vault, port: webhookPort(this.state),
|
|
93
|
-
|
|
98
|
+
// The listener owns admission, including complete bodies still verifying
|
|
99
|
+
// when stop begins. Join their real processing result before HTTP success.
|
|
100
|
+
dispatch: (event) => this.dispatchWebhook(event), clock: () => this.clock(), log: this.log,
|
|
94
101
|
});
|
|
95
102
|
this.bridge = await startBridge({ orch: this, socketPath: socketPathFor(this.state.root), log: this.log });
|
|
96
103
|
// the queue already logs a failed event; a voided dispatch must not also
|
|
@@ -106,27 +113,34 @@ export class Orchestrator {
|
|
|
106
113
|
* daemon's own doors keep serving, and the connect is tried again until it holds or the daemon stops.
|
|
107
114
|
*/
|
|
108
115
|
async connectAdapter(platform, adapter) {
|
|
109
|
-
|
|
116
|
+
if (this.stopping) return;
|
|
117
|
+
let joined;
|
|
118
|
+
const pending = new Promise(resolve => { joined = resolve; });
|
|
119
|
+
(this.adapterConnects ??= new Set()).add(pending);
|
|
110
120
|
try {
|
|
111
|
-
status
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
let status;
|
|
122
|
+
try {
|
|
123
|
+
status = await adapter.connect((msg) => this.dispatchLogged(events.inbound({ ...msg, at: msg.at ?? this.clock() })));
|
|
124
|
+
if (status?.state === 'down' && status.configured === false) {
|
|
125
|
+
// not set up (a missing package or credential): no retry can fix it
|
|
126
|
+
this.log('error', `${platform}: not configured, not retried: ${status.error}`);
|
|
127
|
+
await this.dispatchLogged(events.adapterStatus({ platform, state: 'down', error: status.error, configured: false, at: this.clock() }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.log('error', `${platform}: connect failed, retrying in ${ADAPTER_RETRY_MS / 1000}s: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
await this.dispatchLogged(events.adapterStatus({ platform, state: 'down', error: error instanceof Error ? error.message : String(error), at: this.clock() }));
|
|
133
|
+
if (this.stopping) return;
|
|
134
|
+
this.adapterRetries ??= new Map();
|
|
135
|
+
const timer = setTimeout(() => { this.adapterRetries?.delete(platform); void this.connectAdapter(platform, adapter); }, ADAPTER_RETRY_MS);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
this.adapterRetries.set(platform, timer);
|
|
116
138
|
return;
|
|
117
139
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await this.dispatchLogged(events.adapterStatus({ platform, state: '
|
|
121
|
-
|
|
122
|
-
const timer = setTimeout(() => { this.adapterRetries?.delete(platform); void this.connectAdapter(platform, adapter); }, ADAPTER_RETRY_MS);
|
|
123
|
-
timer.unref?.();
|
|
124
|
-
this.adapterRetries.set(platform, timer);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// connected: recording that is the queue's (a failed save is logged there),
|
|
128
|
-
// never a reason to connect the same adapter again
|
|
129
|
-
await this.dispatchLogged(events.adapterStatus({ platform, state: status?.state ?? 'up', at: this.clock() }));
|
|
140
|
+
// connected: recording that is the queue's (a failed save is logged there),
|
|
141
|
+
// never a reason to connect the same adapter again
|
|
142
|
+
await this.dispatchLogged(events.adapterStatus({ platform, state: status?.state ?? 'up', at: this.clock() }));
|
|
143
|
+
} finally { this.adapterConnects.delete(pending); joined(); }
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
/** The port the webhook listener bound (0 in config means "any free port"). */
|
|
@@ -231,27 +245,111 @@ export class Orchestrator {
|
|
|
231
245
|
}
|
|
232
246
|
}
|
|
233
247
|
|
|
234
|
-
|
|
248
|
+
stop() {
|
|
249
|
+
return this.stopPromise ??= this.stopOwned();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async stopOwned() {
|
|
253
|
+
this.stopping = true;
|
|
254
|
+
this.stopErrors = [];
|
|
255
|
+
const attempt = async (name, work) => {
|
|
256
|
+
try { await work(); }
|
|
257
|
+
catch (error) { this.stopErrors.push(new Error(`${name}: ${error.message}`, { cause: error })); }
|
|
258
|
+
};
|
|
235
259
|
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
236
|
-
// Before any runtime closes: each binding records the runtime to resume (§4.7), while adapters can still deliver.
|
|
237
|
-
await this.dispatch(events.stopping(this.clock())).catch((error) => this.log('error', `stopping: ${error.message}`));
|
|
238
260
|
for (const timer of this.adapterRetries?.values() ?? []) clearTimeout(timer);
|
|
239
261
|
this.adapterRetries?.clear();
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
// Unsent worker input has no positive HTTP receipt. Refuse it before
|
|
263
|
+
// joining handlers; a queued turn cannot hold stop waiting for a model.
|
|
264
|
+
for (const admission of this.webhookAdmissions.values()) if (!admission.sending) admission.finish(new WebhookRefusal('orchestrator is stopping; retry the webhook'));
|
|
242
265
|
this.gateAbort?.abort();
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
// close() seals ingress synchronously, before any connect/cleanup wait.
|
|
267
|
+
// Already-admitted bodies retain dispatch authority through verification;
|
|
268
|
+
// their handlers join the queue before its stopping event below.
|
|
269
|
+
const webhooksClosed = this.webhooks ? attempt('webhooks', () => this.webhooks.close()) : Promise.resolve();
|
|
270
|
+
await Promise.allSettled([...(this.adapterConnects ?? [])]);
|
|
271
|
+
await webhooksClosed;
|
|
272
|
+
this.webhooks = null;
|
|
273
|
+
if (this.bridge) { await attempt('bridge', () => this.bridge.close()); this.bridge = null; }
|
|
274
|
+
await attempt('hosted jobs', () => this.drainHosted());
|
|
275
|
+
await attempt('internal completions', () => this.drainInternal());
|
|
276
|
+
// Join admitted work and record the resumable binding before closing runtimes.
|
|
277
|
+
await attempt('stopping', () => this.dispatch(events.stopping(this.clock())));
|
|
278
|
+
for (const rt of new Set([...this.runtimes.values(), ...this.retired.values()])) await attempt('runtime', () => rt.close());
|
|
245
279
|
this.runtimes.clear();
|
|
246
|
-
|
|
247
|
-
|
|
280
|
+
this.retired.clear();
|
|
281
|
+
for (const adapter of Object.values(this.adapters)) await attempt('adapter', () => adapter.close?.());
|
|
282
|
+
await attempt('World reference', () => releaseWorld());
|
|
283
|
+
if (this.stopErrors.length) throw new AggregateError(this.stopErrors, this.stopErrors.map(error => error.message).join('; '));
|
|
248
284
|
}
|
|
249
285
|
|
|
250
286
|
/** Events are applied one at a time, in arrival order; effects run before the next event. */
|
|
251
287
|
dispatch(event) {
|
|
252
|
-
const
|
|
288
|
+
const frame = { children: [] };
|
|
289
|
+
const run = this.queue.then(async () => {
|
|
290
|
+
this.applying = frame;
|
|
291
|
+
try { return await this.apply(event); }
|
|
292
|
+
finally { this.applying = null; }
|
|
293
|
+
});
|
|
253
294
|
this.queue = run.catch((error) => this.log('error', `dispatch: ${error.message}`));
|
|
254
|
-
|
|
295
|
+
// The queue serializes reducer applications, not these joins: a child
|
|
296
|
+
// must be able to run after its parent's apply without waiting on itself.
|
|
297
|
+
const joined = async (result, failures = []) => {
|
|
298
|
+
const children = await Promise.allSettled(frame.children);
|
|
299
|
+
const errors = [...failures, ...children.filter(row => row.status === 'rejected').map(row => row.reason)];
|
|
300
|
+
if (errors.length) throw new AggregateError(errors, errors.map(error => String(error?.message ?? error)).join('; '));
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
303
|
+
return run.then(result => joined(result), error => joined(null, [error]));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** HTTP success for worker input means handoff, never just an in-memory queue. */
|
|
307
|
+
dispatchWebhook(event) {
|
|
308
|
+
if (this.stopping) return Promise.reject(new WebhookRefusal('orchestrator is stopping; retry the webhook'));
|
|
309
|
+
if (!event.verified) return this.dispatch(event);
|
|
310
|
+
const found = findSubscription(this.state, event.name);
|
|
311
|
+
if (!found?.profile.worker || found.sub.residue?.deliver_only === true) return this.dispatch(event);
|
|
312
|
+
const previous = event.delivery_id && [...this.webhookAdmissions.values()].find(row => row.name === event.name && row.delivery_id === event.delivery_id);
|
|
313
|
+
if (previous) return previous.completion;
|
|
314
|
+
const id = randomUUID();
|
|
315
|
+
let settle;
|
|
316
|
+
const admission = { name: event.name, delivery_id: event.delivery_id, result: new Promise(resolve => { settle = resolve; }) };
|
|
317
|
+
admission.finish = error => {
|
|
318
|
+
if (admission.outcome) return;
|
|
319
|
+
admission.outcome = { error };
|
|
320
|
+
settle(admission.outcome);
|
|
321
|
+
};
|
|
322
|
+
this.webhookAdmissions.set(id, admission);
|
|
323
|
+
admission.completion = (async () => {
|
|
324
|
+
try {
|
|
325
|
+
await this.dispatch({ ...event, admission_id: id });
|
|
326
|
+
if (!admission.awaitingInput) admission.finish(null);
|
|
327
|
+
const { error } = await admission.result;
|
|
328
|
+
if (error) throw error;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// A retryable refusal must not leave an in-memory dedup success for
|
|
331
|
+
// input that was never handed off. Concurrent retries join above.
|
|
332
|
+
try {
|
|
333
|
+
await this.dispatch({ type: 'webhook_refused', name: event.name, delivery_id: event.delivery_id, admission_id: id,
|
|
334
|
+
profile: admission.profile, keyString: admission.keyString, handed: Boolean(admission.outcome && !admission.outcome.error), stopping: Boolean(this.stopping), at: this.clock() });
|
|
335
|
+
} catch (cleanupError) { throw new AggregateError([error, cleanupError], 'webhook refusal cleanup failed'); }
|
|
336
|
+
throw error;
|
|
337
|
+
} finally { this.webhookAdmissions.delete(id); }
|
|
338
|
+
})();
|
|
339
|
+
return admission.completion;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Drain descendants to a stable queue tail, then fence later worker callbacks. */
|
|
343
|
+
async drainInternal() {
|
|
344
|
+
for (;;) {
|
|
345
|
+
const tail = this.queue;
|
|
346
|
+
await Promise.allSettled([tail, ...this.internalDispatches]);
|
|
347
|
+
if (tail === this.queue && !this.internalDispatches.size) {
|
|
348
|
+
this.internalClosed = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (this.internalFailures.length) throw new AggregateError(this.internalFailures, 'internal completion failed');
|
|
255
353
|
}
|
|
256
354
|
|
|
257
355
|
/**
|
|
@@ -260,19 +358,40 @@ export class Orchestrator {
|
|
|
260
358
|
* event currently being applied, i.e. on itself.
|
|
261
359
|
*/
|
|
262
360
|
enqueue(event) {
|
|
263
|
-
|
|
361
|
+
if (this.internalClosed) return;
|
|
362
|
+
const pending = this.dispatch(event);
|
|
363
|
+
this.applying?.children.push(pending);
|
|
364
|
+
this.internalDispatches.add(pending);
|
|
365
|
+
void pending.then(() => this.internalDispatches.delete(pending), error => {
|
|
366
|
+
this.internalFailures.push(error);
|
|
367
|
+
this.internalDispatches.delete(pending);
|
|
368
|
+
});
|
|
264
369
|
}
|
|
265
370
|
|
|
266
371
|
/**
|
|
267
372
|
* `dispatch` for a caller that drops the promise (a timer, an adapter's
|
|
268
|
-
* inbound callback
|
|
373
|
+
* inbound callback). Webhooks retain their dispatch result for HTTP admission.
|
|
374
|
+
* The queue already logged the
|
|
269
375
|
* failure; an unhandled rejection is fatal to Node and stopped the daemon.
|
|
270
376
|
*/
|
|
271
377
|
dispatchLogged(event) {
|
|
378
|
+
if (this.stopping) return Promise.resolve();
|
|
272
379
|
return this.dispatch(event).catch(() => undefined);
|
|
273
380
|
}
|
|
274
381
|
|
|
275
382
|
async apply(event) {
|
|
383
|
+
if (event.type === 'webhook' && this.webhookAdmissions.get(event.admission_id)?.outcome?.error) return [];
|
|
384
|
+
if (event.type === 'worker' && ['error', 'ended'].includes(event.ev.kind)) {
|
|
385
|
+
const binding = this.state.profiles[event.profile]?.bindings?.[event.keyString];
|
|
386
|
+
const current = this.state.runtime?.leases?.[leaseId(event.profile, event.keyString)]?.runtime_id ?? binding?.residue?.runtime_id ?? null;
|
|
387
|
+
if (!event.runtime_id || event.runtime_id === current) {
|
|
388
|
+
for (const admission of this.webhookAdmissions.values()) {
|
|
389
|
+
if (admission.profile === event.profile && admission.keyString === event.keyString) {
|
|
390
|
+
admission.finish(new Error(`webhook worker ended before handoff: ${event.ev.message ?? event.ev.reason ?? 'worker ended'}`));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
276
395
|
if(event.type==='inbound'&&!event.is_me) {
|
|
277
396
|
// An adapter's display name or supplied principal never proves identity.
|
|
278
397
|
const observation=`${event.key.platform}:${event.sender}`;
|
|
@@ -293,11 +412,29 @@ export class Orchestrator {
|
|
|
293
412
|
if (index === firstSave) continue;
|
|
294
413
|
await this.perform(effect);
|
|
295
414
|
}
|
|
415
|
+
// Expiry/reset/end can remove a queued input before its handoff. Its HTTP
|
|
416
|
+
// owner must receive refusal rather than wait on a queue that no longer exists.
|
|
417
|
+
for (const [id, admission] of this.webhookAdmissions) {
|
|
418
|
+
if (!admission.awaitingInput || admission.outcome || admission.sending) continue;
|
|
419
|
+
const lid = leaseId(admission.profile, admission.keyString);
|
|
420
|
+
const rt = this.state.runtime;
|
|
421
|
+
if (rt?.turns?.[lid]?.webhook_input_id !== id && !rt?.queues?.[lid]?.some(row => row.webhook_input_id === id)) {
|
|
422
|
+
admission.finish(new WebhookRefusal('worker input ended before handoff; retry the webhook'));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
296
425
|
return effects;
|
|
297
426
|
}
|
|
298
427
|
|
|
299
428
|
async perform(effect) {
|
|
300
429
|
switch (effect.type) {
|
|
430
|
+
case 'webhook_input': {
|
|
431
|
+
const admission = this.webhookAdmissions.get(effect.admission_id);
|
|
432
|
+
if (admission) {
|
|
433
|
+
Object.assign(admission, { awaitingInput: true, profile: effect.profile, keyString: effect.keyString });
|
|
434
|
+
if (effect.refusal) admission.finish(new WebhookRefusal(effect.refusal));
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
301
438
|
case 'execute_job': return this.executeHostedJob(effect);
|
|
302
439
|
case 'run_gate': return this.runGate(effect);
|
|
303
440
|
case 'runtime_start': return this.startRuntime(effect);
|
|
@@ -382,7 +519,8 @@ export class Orchestrator {
|
|
|
382
519
|
const args = ['open', '--new', program, '--detach', '--window=true', '--on', machine, '--key', `cron-${fire.id}`, '--input', job.prompt ?? ''];
|
|
383
520
|
if (job.workdir && job.workdir.startsWith('/')) args.push('--cwd', job.workdir);
|
|
384
521
|
if (job.model) args.push('--', '--model', job.model);
|
|
385
|
-
const
|
|
522
|
+
const selected = supercodeInvocation(process.env.SUPERCODE_BIN || 'supercode', args);
|
|
523
|
+
const { stdout } = await promisify(execFile)(selected.program, selected.arguments, { timeout: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
386
524
|
const pane = /"pane"\s*:\s*"([^"]+)"/.exec(stdout)?.[1] ?? null;
|
|
387
525
|
return { accepted: true, output: `Fire ${fire.id} of ${job.id} launched on ${machine}${pane ? ` in pane ${pane}` : ''}.`, machine, pane };
|
|
388
526
|
}
|
|
@@ -415,7 +553,7 @@ export class Orchestrator {
|
|
|
415
553
|
return { monitor, script };
|
|
416
554
|
})()
|
|
417
555
|
.catch((error) => ({ script: { ok: false, output: `Script execution failed: ${error.message}` } }))
|
|
418
|
-
.then((answer) => (signal.aborted
|
|
556
|
+
.then((answer) => { if (!signal.aborted) this.enqueue(events.gateResult({ profile: name, fire_id, at: this.clock(), ...answer })); })
|
|
419
557
|
.finally(() => this.hostedTasks.delete(task));
|
|
420
558
|
this.hostedTasks.add(task);
|
|
421
559
|
}
|
|
@@ -494,8 +632,8 @@ export class Orchestrator {
|
|
|
494
632
|
if (resume_runtime_id) resume_runtime_id = await this.continueFromHermes(profile, keyString, worker, resume_runtime_id, cwd,agentHome);
|
|
495
633
|
// read before the runtime exists: a runtime emits from the moment it is made
|
|
496
634
|
steer = Boolean((await this.descriptorFor(worker.harness).catch(() => null))?.runtime?.capabilities?.steer);
|
|
497
|
-
const policy = approvalPolicy(profile, worker.harness);
|
|
498
|
-
const params = { harness: worker.harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) };
|
|
635
|
+
const policy = approvalPolicy(profile, worker.harness), reviewer = approvalsReviewer(profile, worker.harness);
|
|
636
|
+
const params = { harness: worker.harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}), ...(reviewer ? { approvals_reviewer: reviewer } : {}) };
|
|
499
637
|
if (resume_runtime_id && (await this.descriptorFor(worker.harness).catch(() => null))?.runtime?.capabilities?.resume_session) {
|
|
500
638
|
try {
|
|
501
639
|
runtime = await this.client.resumeManagedRuntime({ ...params, runtime_id: resume_runtime_id });
|
|
@@ -581,9 +719,12 @@ export class Orchestrator {
|
|
|
581
719
|
const instructions = agentPreamble(agent, { main: true,
|
|
582
720
|
previous: localAgentSessions(this.state.root, scope.org, agent.identity.id) });
|
|
583
721
|
const arguments_ = [...route.args, ...INSTRUCTION_ARGUMENTS.codex(instructions)];
|
|
722
|
+
// The profile's mode, on a resume too: an approvals-on main asks a person (`on-request`, reviewer `user`), an
|
|
723
|
+
// approvals-off main gets the auto-reviewer again (a resume's recorded mode gives way to it: teams/mail/agents.mjs
|
|
724
|
+
// `resume`).
|
|
725
|
+
arguments_.push(...mainApprovalArguments(profile));
|
|
584
726
|
if (resume === null) {
|
|
585
|
-
arguments_.push(...modelArguments({ ...worker, model: route.model })
|
|
586
|
-
arguments_.push(...sandboxArguments(profile, 'codex'));
|
|
727
|
+
arguments_.push(...modelArguments({ ...worker, model: route.model }));
|
|
587
728
|
if (worker.reasoning_effort) arguments_.push('-c', `model_reasoning_effort=${JSON.stringify(worker.reasoning_effort)}`);
|
|
588
729
|
}
|
|
589
730
|
declareWorkerHome({ harness: 'codex', env, cwd: folder, log: (level, text) => this.log(level, text) });
|
|
@@ -712,7 +853,7 @@ export class Orchestrator {
|
|
|
712
853
|
async resolveLaunch(profile, worker, env, routeArgs = []) {
|
|
713
854
|
if (this.launchFor) {
|
|
714
855
|
const override = this.launchFor(profile, worker, env);
|
|
715
|
-
if (override) return { ...override, arguments: [...(override.arguments ?? []), ...modelArguments(worker), ...routeArgs, ...
|
|
856
|
+
if (override) return { ...override, arguments: [...(override.arguments ?? []), ...modelArguments(worker), ...routeArgs, ...modeArguments(profile, worker.harness), ...limitArguments(profile, worker, this.log)] };
|
|
716
857
|
}
|
|
717
858
|
const base = (await this.descriptorFor(worker.harness))?.runtime?.default_launch;
|
|
718
859
|
if (!base) {
|
|
@@ -720,7 +861,7 @@ export class Orchestrator {
|
|
|
720
861
|
if (worker.model) throw new Error(`${worker.harness} publishes no launch to carry model ${worker.model}`);
|
|
721
862
|
return undefined;
|
|
722
863
|
}
|
|
723
|
-
return { program: base.program, arguments: [...(base.arguments ?? []), ...modelArguments(worker), ...routeArgs, ...
|
|
864
|
+
return { program: base.program, arguments: [...(base.arguments ?? []), ...modelArguments(worker), ...routeArgs, ...modeArguments(profile, worker.harness), ...limitArguments(profile, worker, this.log)], env: { ...(base.env ?? {}), ...env } };
|
|
724
865
|
}
|
|
725
866
|
|
|
726
867
|
/**
|
|
@@ -904,9 +1045,13 @@ export class Orchestrator {
|
|
|
904
1045
|
} catch (error) { this.log('warn', `transfer history unread: ${error.message}`); return ''; }
|
|
905
1046
|
}
|
|
906
1047
|
|
|
907
|
-
async sendInput({ profile, keyString, text, context_from = null, skills = [], job_id = null, history = null }) {
|
|
1048
|
+
async sendInput({ profile, keyString, text, context_from = null, skills = [], job_id = null, history = null, webhook_input_id = null }) {
|
|
1049
|
+
const admission = webhook_input_id ? this.webhookAdmissions.get(webhook_input_id) : null;
|
|
1050
|
+
// Shutdown refused this unsent request (or its handler already failed).
|
|
1051
|
+
// It must not be delivered later by a queued started/final completion.
|
|
1052
|
+
if (webhook_input_id && (!admission || admission.outcome?.error)) return;
|
|
908
1053
|
const runtime = this.runtimes.get(leaseId(profile, keyString));
|
|
909
|
-
if (!runtime) { this.log('warn', `send_input with no runtime for ${keyString}`); return; }
|
|
1054
|
+
if (!runtime) { admission?.finish(new Error(`webhook input has no runtime for ${keyString}`)); this.log('warn', `send_input with no runtime for ${keyString}`); return; }
|
|
910
1055
|
const owner = this.state.profiles[profile];
|
|
911
1056
|
const dir = owner?.dir;
|
|
912
1057
|
let input = context_from && dir ? contextFromPrefix(dir, job_id, context_from) + text : text;
|
|
@@ -914,19 +1059,25 @@ export class Orchestrator {
|
|
|
914
1059
|
if (skills?.length && owner) input = await this.skillsPrefix(owner, skills, owner.jobs?.[job_id]?.workdir ?? null, input) ?? input;
|
|
915
1060
|
const lid=leaseId(profile,keyString),preamble=this.runtimeAgentPreambles?.get(lid);
|
|
916
1061
|
if(preamble)input=`${preamble}\n\n${input}`;
|
|
1062
|
+
if (admission?.outcome?.error) return;
|
|
1063
|
+
if (admission) admission.sending = true;
|
|
917
1064
|
try {
|
|
918
1065
|
await runtime.sendInput(input);
|
|
919
1066
|
this.runtimeAgentPreambles?.delete(lid);
|
|
1067
|
+
admission?.finish(null);
|
|
920
1068
|
} catch (error) {
|
|
1069
|
+
if (admission) admission.sending = false;
|
|
921
1070
|
// A turn the harness is still running (one this loop did not start, or whose end it has not heard yet) is not a
|
|
922
1071
|
// dead session: the line waits for that turn's end, as a refused steer does. Ending the binding on it killed a
|
|
923
1072
|
// live lobby manager on the first message that reached it mid-turn.
|
|
924
1073
|
if (/already in progress/.test(error.message)) {
|
|
1074
|
+
if (admission && this.stopping) { admission.finish(new WebhookRefusal('orchestrator stopped before the worker could take this input; retry')); return; }
|
|
925
1075
|
this.log('info', `send ${keyString}: a turn is in progress; the line waits for its end`);
|
|
926
|
-
this.enqueue(events.worker({ profile, keyString, runtime_id: runtime.handle.runtime_id, ev: { kind: 'steer_refused', text: input }, at: this.clock() }));
|
|
1076
|
+
this.enqueue(events.worker({ profile, keyString, runtime_id: runtime.handle.runtime_id, ev: { kind: 'steer_refused', text: webhook_input_id ? text : input, webhook_input_id }, at: this.clock() }));
|
|
927
1077
|
return;
|
|
928
1078
|
}
|
|
929
1079
|
this.enqueue(events.worker({ profile, keyString, runtime_id: runtime.handle.runtime_id, ev: { kind: 'error', message: `send failed: ${error.message}` }, at: this.clock() }));
|
|
1080
|
+
admission?.finish(error);
|
|
930
1081
|
}
|
|
931
1082
|
}
|
|
932
1083
|
|
|
@@ -982,14 +1133,14 @@ export class Orchestrator {
|
|
|
982
1133
|
const old = this.retired.get(runtime_id);
|
|
983
1134
|
this.retired.delete(runtime_id);
|
|
984
1135
|
this.turnCounters?.delete(runtime_id);
|
|
985
|
-
try { await old.close(); } catch {
|
|
1136
|
+
try { await old.close(); } catch (error) { if (this.stopping) this.stopErrors.push(error); }
|
|
986
1137
|
return;
|
|
987
1138
|
}
|
|
988
1139
|
const runtime = this.runtimes.get(lid);
|
|
989
1140
|
if (runtime_id && runtime && runtime.handle.runtime_id !== runtime_id) return;
|
|
990
1141
|
this.runtimes.delete(lid);
|
|
991
1142
|
if (runtime) this.turnCounters?.delete(runtime.handle.runtime_id);
|
|
992
|
-
if (runtime) { try { await runtime.close(); } catch {
|
|
1143
|
+
if (runtime) { try { await runtime.close(); } catch (error) { if (this.stopping) this.stopErrors.push(error); } }
|
|
993
1144
|
}
|
|
994
1145
|
|
|
995
1146
|
/**
|
package/mcp/mount.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { SERVER_NAME } from './tools.mjs';
|
|
|
23
23
|
import { SOCKET_ENV, socketPathFor } from './server.mjs';
|
|
24
24
|
import { mcpServers } from '../hermes-home.mjs';
|
|
25
25
|
import { KANBAN_ENV } from '../hermes-cli.mjs';
|
|
26
|
-
import {
|
|
26
|
+
import { installedAgentRecords } from '../installed-apps.mjs';
|
|
27
27
|
import { supercodeHome } from '@volter/teams/home';
|
|
28
28
|
|
|
29
29
|
export { MCP_DOORS, doorFor } from '@volter/supercode-harness-sdk/mcp-doors';
|
|
@@ -41,11 +41,12 @@ function stableSupercode() {
|
|
|
41
41
|
* `rh2` server, whatever it holds, is neither replaced nor relied on). */
|
|
42
42
|
export const RH2_AGENT_SERVER = 'rh2-agent';
|
|
43
43
|
|
|
44
|
-
/** `rh2 mcp` run as the agent's installed RH2 principal: `apps mcp` puts
|
|
45
|
-
* its environment,
|
|
46
|
-
* Teams id): a record is found by that id alone, so an
|
|
44
|
+
/** `rh2 mcp` run as the agent's installed RH2 principal: `apps mcp` puts its first record's origin, organization and
|
|
45
|
+
* token in its environment, and the home whose records hold its other Workplaces', so no config file carries a token.
|
|
46
|
+
* Null when no rh2-agent record is an identity of `agentId` (its Teams id): a record is found by that id alone, so an
|
|
47
|
+
* agent sharing another's name is never mounted with its record. */
|
|
47
48
|
function rh2AgentServer(root, agentId, program) {
|
|
48
|
-
if (!
|
|
49
|
+
if (!installedAgentRecords(root, agentId).length) return null;
|
|
49
50
|
return { command: stableSupercode(), args: ['orchestrator', 'apps', 'mcp', 'rh2-agent', '--root', root, '--agent', agentId, '--node', program] };
|
|
50
51
|
}
|
|
51
52
|
|
package/orchestration.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The folder, through the harness's orchestration doors.
|
|
2
2
|
//
|
|
3
3
|
// ONT-7: this package reads and writes its home through `harness.v1.orchestration.*`
|
|
4
|
-
// (
|
|
5
|
-
// ontology's one codec is
|
|
4
|
+
// (the SDK's orchestration owners), never with a codec of its own. The
|
|
5
|
+
// ontology's one codec is TypeScript; this module is its client side:
|
|
6
6
|
//
|
|
7
7
|
// - `load(root)` asks `orchestration.load` for the orchestration value and adds the runtime
|
|
8
8
|
// half (`OrchestratorState` = orchestration + runtime, docs/ORCHESTRATOR-IR.md §2.1).
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
// moves credentials, so `import` and `export` are composed inside the
|
|
15
15
|
// harness process (see the orchestration_doors module doc).
|
|
16
16
|
//
|
|
17
|
-
// One door per process: the first verb
|
|
18
|
-
// (the binary that spawned us, else this checkout's build) and every verb
|
|
17
|
+
// One door per process: the first verb opens the SDK's owned transport and every verb
|
|
19
18
|
// after it reuses it. The door is held open while a loop runs
|
|
20
19
|
// (`retainWorld` / `releaseWorld`, the loop's `start` / `stop`) or a verb is
|
|
21
20
|
// in flight; otherwise it closes itself after a short idle, so a process
|
|
@@ -23,26 +22,21 @@
|
|
|
23
22
|
// `closeWorld()` closes it now.
|
|
24
23
|
|
|
25
24
|
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
-
import {
|
|
27
|
-
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { join } from 'node:path';
|
|
28
26
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
29
27
|
import { emptyRuntime, persistedView } from './ir.mjs';
|
|
30
|
-
import {
|
|
28
|
+
import { lockedProgram } from './source-lock.mjs';
|
|
29
|
+
import { sourceRoot } from '@volter/supercode-teams/source-lock';
|
|
31
30
|
import { SESSION_READ_REQUEST_UNITS, stringifySessionWire } from '@volter/supercode-teams/session-read-client';
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The binary the doors are opened with: `SUPERCODE_BIN` (set by `supercode
|
|
37
|
-
* orchestrator start` to the very binary that spawned the daemon), else this
|
|
38
|
-
* checkout's debug build, else `supercode` on PATH.
|
|
39
|
-
*/
|
|
32
|
+
/** Selected CLI for clients that need it: the locked Node entry, explicit
|
|
33
|
+
* override, this source tree's front door, or the installed PATH command. */
|
|
40
34
|
export function supercodeCommand() {
|
|
41
|
-
const pinned =
|
|
35
|
+
const pinned = lockedProgram();
|
|
42
36
|
if (pinned) return pinned;
|
|
43
37
|
if (process.env.SUPERCODE_BIN) return process.env.SUPERCODE_BIN;
|
|
44
|
-
const
|
|
45
|
-
return
|
|
38
|
+
const root = sourceRoot(import.meta.url);
|
|
39
|
+
return root ? join(root, 'packaging/npm/bin/supercode.js') : 'supercode';
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
const IDLE_MS = 250;
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.216",
|
|
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": {
|
|
7
|
+
"./daemon-control": {
|
|
8
|
+
"types": "./daemon-control.d.ts",
|
|
9
|
+
"import": "./daemon-control.mjs"
|
|
10
|
+
},
|
|
7
11
|
"./session-recovery": {
|
|
8
12
|
"types": "./session-recovery.d.ts",
|
|
9
13
|
"import": "./session-recovery.mjs"
|
|
@@ -32,10 +36,10 @@
|
|
|
32
36
|
"check-types": "node scripts/generate-types.mjs --check"
|
|
33
37
|
},
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@volter/supercode-teams": "^0.3.
|
|
36
|
-
"@volter/supercode-terminal": "^0.3.
|
|
37
|
-
"@volter/supercode-harness-sdk": "^0.3.
|
|
38
|
-
"@volter/teams": "0.2.
|
|
39
|
+
"@volter/supercode-teams": "^0.3.355",
|
|
40
|
+
"@volter/supercode-terminal": "^0.3.12",
|
|
41
|
+
"@volter/supercode-harness-sdk": "^0.3.106",
|
|
42
|
+
"@volter/teams": "0.2.102",
|
|
39
43
|
"@volter/ztrack": "^2.4.23",
|
|
40
44
|
"yaml": "^2.9.0"
|
|
41
45
|
},
|
|
@@ -61,6 +65,5 @@
|
|
|
61
65
|
"board",
|
|
62
66
|
"apply",
|
|
63
67
|
"README.md"
|
|
64
|
-
]
|
|
65
|
-
"gitHead": "2455341d186426df67c39d16ad03fdb021e2fe8e"
|
|
68
|
+
]
|
|
66
69
|
}
|
package/reducer.mjs
CHANGED
|
@@ -499,7 +499,7 @@ function handleWorker(state, ev, effects) {
|
|
|
499
499
|
const q = rt.queues[lid] ?? [];
|
|
500
500
|
if (q.length && !rt.turns[lid]) {
|
|
501
501
|
const next = q.shift();
|
|
502
|
-
rt.turns[lid] = { started_at: now, buffer: '' };
|
|
502
|
+
rt.turns[lid] = { started_at: now, buffer: '', ...(next.webhook_input_id ? { webhook_input_id: next.webhook_input_id } : {}) };
|
|
503
503
|
effects.push(queuedInput(profile.name, ev.keyString, next));
|
|
504
504
|
}
|
|
505
505
|
effects.push({ type: 'save' });
|
|
@@ -507,7 +507,7 @@ function handleWorker(state, ev, effects) {
|
|
|
507
507
|
}
|
|
508
508
|
case 'steer_refused': {
|
|
509
509
|
// The harness would not take the line mid-turn: it waits for the turn's end like any other.
|
|
510
|
-
(rt.queues[lid] ??= []).push({ text: w.text, at: now });
|
|
510
|
+
(rt.queues[lid] ??= []).push({ text: w.text, at: now, ...(w.webhook_input_id ? { webhook_input_id: w.webhook_input_id } : {}) });
|
|
511
511
|
break;
|
|
512
512
|
}
|
|
513
513
|
case 'text': {
|
|
@@ -556,7 +556,7 @@ function handleWorker(state, ev, effects) {
|
|
|
556
556
|
const q = rt.queues[lid] ?? [];
|
|
557
557
|
if (q.length && rt.leases[lid]) {
|
|
558
558
|
const next = q.shift();
|
|
559
|
-
rt.turns[lid] = { started_at: now, buffer: '' };
|
|
559
|
+
rt.turns[lid] = { started_at: now, buffer: '', ...(next.webhook_input_id ? { webhook_input_id: next.webhook_input_id } : {}) };
|
|
560
560
|
effects.push(queuedInput(profile.name, ev.keyString, next));
|
|
561
561
|
}
|
|
562
562
|
// G3: the session's usage is read again from the harness's own records after every turn
|
|
@@ -630,7 +630,28 @@ export function permissionResponse(options, allow, reason = null) {
|
|
|
630
630
|
*/
|
|
631
631
|
function queuedInput(profile, keyString, next) {
|
|
632
632
|
const framed = next.job_id && (next.context_from || next.skills?.length);
|
|
633
|
-
return { type: 'send_input', profile, keyString, text: next.text, ...(framed ? { context_from: next.context_from ?? null, skills: next.skills ?? [], job_id: next.job_id } : {}), ...(next.history ? { history: next.history } : {}) };
|
|
633
|
+
return { type: 'send_input', profile, keyString, text: next.text, ...(framed ? { context_from: next.context_from ?? null, skills: next.skills ?? [], job_id: next.job_id } : {}), ...(next.history ? { history: next.history } : {}), ...(next.webhook_input_id ? { webhook_input_id: next.webhook_input_id } : {}) };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Remove only this unacknowledged input; a refused queue slot is no live turn. */
|
|
637
|
+
function refuseWebhook(state, ev, effects) {
|
|
638
|
+
if (ev.handed) return;
|
|
639
|
+
// Only an apply that claimed this input may retire its dedup entry. A
|
|
640
|
+
// request refused before apply must not erase an earlier successful delivery.
|
|
641
|
+
if (!ev.profile || !ev.keyString) return;
|
|
642
|
+
const rt = ensureRuntime(state);
|
|
643
|
+
const seen = rt.webhook_deliveries?.[ev.name];
|
|
644
|
+
const index = seen?.indexOf(ev.delivery_id) ?? -1;
|
|
645
|
+
if (index !== -1) seen.splice(index, 1);
|
|
646
|
+
const lid = leaseId(ev.profile, ev.keyString);
|
|
647
|
+
if (rt.queues[lid]) rt.queues[lid] = rt.queues[lid].filter(row => row.webhook_input_id !== ev.admission_id);
|
|
648
|
+
if (rt.turns[lid]?.webhook_input_id !== ev.admission_id) return;
|
|
649
|
+
delete rt.turns[lid];
|
|
650
|
+
if (!ev.stopping && rt.leases[lid] && rt.queues[lid]?.length) {
|
|
651
|
+
const next = rt.queues[lid].shift();
|
|
652
|
+
rt.turns[lid] = { started_at: ev.at, buffer: '', ...(next.webhook_input_id ? { webhook_input_id: next.webhook_input_id } : {}) };
|
|
653
|
+
effects.push(queuedInput(ev.profile, ev.keyString, next));
|
|
654
|
+
}
|
|
634
655
|
}
|
|
635
656
|
|
|
636
657
|
/** An admin's numeric reply resolves the oldest pending request on that surface. */
|
|
@@ -688,10 +709,13 @@ function handleStopping(state, ev, effects) {
|
|
|
688
709
|
delete b.residue.runtime_id;
|
|
689
710
|
// a stop for a restart: what was live now is live again once the daemon is back (handleRestart)
|
|
690
711
|
if (b.key.platform !== 'webhook' && b.key.platform !== 'cron') b.residue.reopen_at_start = true;
|
|
691
|
-
if (rt.turns[leaseId(profile.name, ks)] && !b.recurrence
|
|
712
|
+
if (rt.turns[leaseId(profile.name, ks)] && !b.recurrence) {
|
|
692
713
|
b.residue.interrupted_at = ev.at;
|
|
693
|
-
reply(state, profile, b.key, REPLY.interrupted, ev.at, effects);
|
|
714
|
+
if (b.key.platform !== 'webhook' && b.key.platform !== 'cron') reply(state, profile, b.key, REPLY.interrupted, ev.at, effects);
|
|
694
715
|
}
|
|
716
|
+
// the runtime the stop is about to close no longer serves the surface: its `closed` is not heard (handleWorker),
|
|
717
|
+
// so it cannot end a binding, or fail a fire, that the next daemon resumes
|
|
718
|
+
delete rt.leases[leaseId(profile.name, ks)];
|
|
695
719
|
}
|
|
696
720
|
}
|
|
697
721
|
effects.push({ type: 'save' });
|
|
@@ -962,6 +986,7 @@ export function step(state, event) {
|
|
|
962
986
|
case 'hosted_result': finishHostedFire(state, event, effects); break;
|
|
963
987
|
case 'gate_result': gateResult(state, event, effects); break;
|
|
964
988
|
case 'webhook': handleWebhook(state, event, effects); break;
|
|
989
|
+
case 'webhook_refused': refuseWebhook(state, event, effects); break;
|
|
965
990
|
case 'restart': handleRestart(state, event, effects); break;
|
|
966
991
|
case 'stopping': handleStopping(state, event, effects); break;
|
|
967
992
|
case 'adapter': handleAdapter(state, event); break;
|