agents-relay 2.0.58 → 2.0.59
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/dist/adapters.js +15 -34
- package/dist/cli-escalation.js +4 -4
- package/dist/cli.js +24 -10
- package/dist/continuation.js +3 -3
- package/dist/decision.js +5 -5
- package/dist/github-client.js +8 -8
- package/dist/owned-process.js +170 -0
- package/dist/planner.js +8 -10
- package/dist/reconciler-runtime.js +18 -4
- package/dist/relayd.js +2 -1
- package/dist/usage.js +16 -10
- package/dist/workspace.js +14 -5
- package/package.json +1 -1
package/dist/adapters.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
5
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { spawnOwnedProcess, terminateOwnedProcess } from './owned-process.js';
|
|
6
6
|
export function buildCodexArgs(route, input) {
|
|
7
7
|
const args = ['exec', '--json'];
|
|
8
8
|
if (route.profile)
|
|
@@ -27,29 +27,6 @@ function parseThreadStarted(line) {
|
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
function signalChildTree(child, signal) {
|
|
31
|
-
if (child.pid && process.platform !== 'win32') {
|
|
32
|
-
try {
|
|
33
|
-
process.kill(-child.pid, signal);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (error.code !== 'ESRCH')
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
child.kill(signal);
|
|
42
|
-
}
|
|
43
|
-
function terminateChild(child, graceMs = 750) {
|
|
44
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
45
|
-
return;
|
|
46
|
-
signalChildTree(child, 'SIGTERM');
|
|
47
|
-
const force = setTimeout(() => {
|
|
48
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
49
|
-
signalChildTree(child, 'SIGKILL');
|
|
50
|
-
}, graceMs);
|
|
51
|
-
force.unref();
|
|
52
|
-
}
|
|
53
30
|
function appendTail(current, chunk, maxBytes = 16384) {
|
|
54
31
|
const combined = current + chunk;
|
|
55
32
|
return Buffer.byteLength(combined) <= maxBytes
|
|
@@ -67,11 +44,11 @@ export class CodexAdapter {
|
|
|
67
44
|
launch(task, signal, context = {}) {
|
|
68
45
|
if (!task.routing)
|
|
69
46
|
throw new Error(`Task ${task.id} requires routing metadata before Codex launch`);
|
|
70
|
-
const child =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
47
|
+
const child = spawnOwnedProcess(this.command, buildCodexArgs(task.routing, task.input), {
|
|
48
|
+
invocation: `worker.codex:${task.id}`,
|
|
49
|
+
signal,
|
|
50
|
+
correlation: { jobId: task.jobId, taskId: task.id, executionId: task.executionId ?? undefined },
|
|
51
|
+
spawn: { shell: false, cwd: task.routing.cwd, stdio: ['ignore', 'pipe', 'pipe'] },
|
|
75
52
|
});
|
|
76
53
|
let stdoutBuffer = '';
|
|
77
54
|
let stderrTail = '';
|
|
@@ -104,7 +81,8 @@ export class CodexAdapter {
|
|
|
104
81
|
const promise = new Promise((resolve, reject) => {
|
|
105
82
|
child.on('error', error => { launchedReject(error); reject(error); });
|
|
106
83
|
child.once('spawn', launchedResolve);
|
|
107
|
-
child.on('close', code => {
|
|
84
|
+
child.on('close', (code, signal) => {
|
|
85
|
+
execution.processExit = { exitCode: code, signal, stderr: stderrTail };
|
|
108
86
|
if (stdoutBuffer)
|
|
109
87
|
consumeLine(stdoutBuffer);
|
|
110
88
|
if (code === 0) {
|
|
@@ -114,9 +92,8 @@ export class CodexAdapter {
|
|
|
114
92
|
reject(new Error(stderrTail.trim() || `Codex exited ${code}`));
|
|
115
93
|
}
|
|
116
94
|
});
|
|
117
|
-
signal.addEventListener('abort', () => terminateChild(child), { once: true });
|
|
118
95
|
});
|
|
119
|
-
execution = { id: randomUUID(), promise, launched, cancel: () =>
|
|
96
|
+
execution = { id: randomUUID(), promise, launched, cancel: () => { void terminateOwnedProcess(child, 'cancel'); } };
|
|
120
97
|
execution.threadId = bufferedThreadId;
|
|
121
98
|
return execution;
|
|
122
99
|
}
|
|
@@ -153,10 +130,14 @@ function browserWorkspaceCli(override = process.env.AGENTS_RELAY_BROWSER_WORKSPA
|
|
|
153
130
|
}
|
|
154
131
|
function browserWorkspaceCommand(cli, args, stdin, signal) {
|
|
155
132
|
return new Promise((resolve, reject) => {
|
|
156
|
-
const child =
|
|
133
|
+
const child = spawnOwnedProcess(cli.command, [...cli.prefix, ...args], {
|
|
134
|
+
invocation: 'browser-workspace.command',
|
|
135
|
+
signal,
|
|
136
|
+
spawn: { shell: false, stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'] },
|
|
137
|
+
});
|
|
157
138
|
let stdout = '';
|
|
158
139
|
let stderr = '';
|
|
159
|
-
const abort = () =>
|
|
140
|
+
const abort = () => { void terminateOwnedProcess(child, 'abort'); };
|
|
160
141
|
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
161
142
|
child.stderr?.on('data', (chunk) => { stderr = appendTail(stderr, chunk.toString()); });
|
|
162
143
|
child.on('error', reject);
|
package/dist/cli-escalation.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
2
|
import { tmpdir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { textOption, arg } from './cli-support.js';
|
|
6
5
|
import { runEscalationGraph } from './workflow/escalation.js';
|
|
6
|
+
import { spawnOwnedProcess } from './owned-process.js';
|
|
7
7
|
export function neoEscalationArgs(handoffPath, policyPath, cwd) {
|
|
8
8
|
return [
|
|
9
9
|
'escalation', '--handoff-file', handoffPath,
|
|
@@ -31,11 +31,11 @@ export async function runNeoEscalation(request) {
|
|
|
31
31
|
}
|
|
32
32
|
const args = neoEscalationArgs(handoffPath, policyPath, request.cwd);
|
|
33
33
|
return await new Promise((resolve, reject) => {
|
|
34
|
-
const child =
|
|
34
|
+
const child = spawnOwnedProcess('neo', args, { invocation: 'escalation.neo', spawn: { cwd: request.cwd ?? process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] } });
|
|
35
35
|
let stdout = '';
|
|
36
36
|
let stderr = '';
|
|
37
|
-
child.stdout
|
|
38
|
-
child.stderr
|
|
37
|
+
child.stdout?.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
|
|
38
|
+
child.stderr?.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
|
|
39
39
|
child.on('error', reject);
|
|
40
40
|
child.on('close', code => {
|
|
41
41
|
let parsed;
|
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import { helpText, usage } from './cli-help.js';
|
|
|
29
29
|
import { parseAgentGraph } from './agent-graph.js';
|
|
30
30
|
import { runJobUnblockCommand, runTaskUnblockCommand } from './cli-unblock.js';
|
|
31
31
|
import { createJobEntity, createTaskEntity } from './workflow/entity-creation.js';
|
|
32
|
+
import { shutdownOwnedProcesses } from './owned-process.js';
|
|
32
33
|
export { helpText } from './cli-help.js';
|
|
33
34
|
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, publishDependentUnblocked, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayTaskCommand, withEventBus } from './cli-support.js';
|
|
34
35
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
@@ -520,7 +521,7 @@ export async function createService(args) {
|
|
|
520
521
|
const serverClosed = new Promise((resolve, reject) => { dashboardServer.close(error => error ? reject(error) : resolve()); });
|
|
521
522
|
dashboardServer.closeAllConnections();
|
|
522
523
|
const watcherClosed = bus ? bus.close() : stopWatcher();
|
|
523
|
-
stopping = Promise.all([watcherClosed, serverClosed]).then(() => undefined, error => { throw error; });
|
|
524
|
+
stopping = Promise.all([reconciler.close(), watcherClosed, serverClosed, shutdownOwnedProcesses('parent-shutdown')]).then(() => undefined, error => { throw error; });
|
|
524
525
|
return stopping;
|
|
525
526
|
}
|
|
526
527
|
};
|
|
@@ -631,10 +632,17 @@ async function main() {
|
|
|
631
632
|
if (command === 'plan') {
|
|
632
633
|
const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
|
|
633
634
|
await withEventBus(args, async (bus) => {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
635
|
+
const reconciler = runtime(store, args, bus);
|
|
636
|
+
try {
|
|
637
|
+
const result = await reconciler.planJob(job.id, guidance?.trim() || undefined);
|
|
638
|
+
await saveLocal(localFile, result.job);
|
|
639
|
+
await publishWake(bus, result.job, result.round.id, `Planning round ${result.round.id} completed`);
|
|
640
|
+
console.log(JSON.stringify(result.round, null, 2));
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
await reconciler.close();
|
|
644
|
+
await shutdownOwnedProcesses('parent-shutdown');
|
|
645
|
+
}
|
|
638
646
|
});
|
|
639
647
|
return;
|
|
640
648
|
}
|
|
@@ -679,11 +687,17 @@ async function main() {
|
|
|
679
687
|
return;
|
|
680
688
|
}
|
|
681
689
|
const reconciler = runtime(store, args, bus);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
690
|
+
try {
|
|
691
|
+
await reconciler.reconcile(job.id);
|
|
692
|
+
await reconciler.idle();
|
|
693
|
+
const finalState = await store.load(job.id);
|
|
694
|
+
await saveLocal(localFile, finalState);
|
|
695
|
+
console.log(JSON.stringify(finalState, null, 2));
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
await reconciler.close();
|
|
699
|
+
await shutdownOwnedProcesses('parent-shutdown');
|
|
700
|
+
}
|
|
687
701
|
});
|
|
688
702
|
return;
|
|
689
703
|
}
|
package/dist/continuation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnOwnedProcess } from './owned-process.js';
|
|
2
2
|
export class CodexThreadContinuation {
|
|
3
3
|
command;
|
|
4
4
|
kind = 'codex-thread';
|
|
@@ -6,12 +6,12 @@ export class CodexThreadContinuation {
|
|
|
6
6
|
this.command = command;
|
|
7
7
|
}
|
|
8
8
|
async continue(input, continuation) { if (continuation.kind !== this.kind)
|
|
9
|
-
throw new Error('Continuation is not a Codex thread'); await new Promise((resolve, reject) => { const child =
|
|
9
|
+
throw new Error('Continuation is not a Codex thread'); await new Promise((resolve, reject) => { const child = spawnOwnedProcess(this.command, ['exec', 'resume', continuation.target, input], { invocation: 'continuation.codex' }); child.on('error', reject); child.on('close', code => code === 0 ? resolve() : reject(new Error(`Codex continuation exited ${code}`))); }); }
|
|
10
10
|
}
|
|
11
11
|
export class CommandContinuation {
|
|
12
12
|
kind = 'command';
|
|
13
13
|
async continue(input, continuation) { if (continuation.kind !== this.kind)
|
|
14
|
-
throw new Error('Continuation is not a command'); await new Promise((resolve, reject) => { const child =
|
|
14
|
+
throw new Error('Continuation is not a command'); await new Promise((resolve, reject) => { const child = spawnOwnedProcess(continuation.target, [input], { invocation: 'continuation.command' }); child.on('error', reject); child.on('close', code => code === 0 ? resolve() : reject(new Error(`Continuation exited ${code}`))); }); }
|
|
15
15
|
}
|
|
16
16
|
export class WebhookContinuation {
|
|
17
17
|
kind = 'webhook';
|
package/dist/decision.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
2
|
import { tmpdir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
4
|
+
import { spawnOwnedProcess, terminateOwnedProcess } from './owned-process.js';
|
|
5
5
|
function validateContract(contract) {
|
|
6
6
|
if (contract.type !== 'choice')
|
|
7
7
|
throw new Error(`Unsupported decision contract ${contract.type}`);
|
|
@@ -122,7 +122,7 @@ export class LayaCliDecisionEngine {
|
|
|
122
122
|
}
|
|
123
123
|
async run(extraArgs) {
|
|
124
124
|
return await new Promise((resolve, reject) => {
|
|
125
|
-
const child =
|
|
125
|
+
const child = spawnOwnedProcess(this.command, [...this.args, ...extraArgs], { invocation: 'decision.laya', spawn: { stdio: ['ignore', 'pipe', 'pipe'], env: this.env ?? process.env } });
|
|
126
126
|
const stdout = [];
|
|
127
127
|
const stderr = [];
|
|
128
128
|
let settled = false;
|
|
@@ -137,11 +137,11 @@ export class LayaCliDecisionEngine {
|
|
|
137
137
|
resolve(output ?? '');
|
|
138
138
|
};
|
|
139
139
|
const timer = setTimeout(() => {
|
|
140
|
-
child
|
|
140
|
+
void terminateOwnedProcess(child, 'timeout', 0);
|
|
141
141
|
finish(new Error(`Laya CLI timed out after ${this.timeoutMs}ms`));
|
|
142
142
|
}, this.timeoutMs);
|
|
143
|
-
child.stdout
|
|
144
|
-
child.stderr
|
|
143
|
+
child.stdout?.on('data', chunk => stdout.push(Buffer.from(chunk)));
|
|
144
|
+
child.stderr?.on('data', chunk => stderr.push(Buffer.from(chunk)));
|
|
145
145
|
child.once('error', error => finish(error));
|
|
146
146
|
child.once('close', code => {
|
|
147
147
|
if (code !== 0) {
|
package/dist/github-client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { createSign } from 'node:crypto';
|
|
3
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
3
|
import { emptyGitHubRuntimeRateUsage, observeRuntimeRateLimit, parseGitHubCliHeaders, runtimeRejection } from './github-runtime-usage.js';
|
|
4
|
+
import { spawnOwnedProcess } from './owned-process.js';
|
|
5
5
|
function base64Url(value) {
|
|
6
6
|
return Buffer.from(value).toString('base64url');
|
|
7
7
|
}
|
|
@@ -143,15 +143,15 @@ export class GhApiClient {
|
|
|
143
143
|
this.assertAvailable();
|
|
144
144
|
const commandArgs = includeHeaders ? [args[0] ?? '', '--include', ...args.slice(1)] : args;
|
|
145
145
|
return await new Promise((resolve) => {
|
|
146
|
-
const child =
|
|
146
|
+
const child = spawnOwnedProcess('gh', commandArgs, { invocation: 'github.api', spawn: { env } });
|
|
147
147
|
let stdout = '';
|
|
148
148
|
let stderr = '';
|
|
149
|
-
child.stdout
|
|
150
|
-
child.stderr
|
|
149
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
150
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
151
151
|
child.on('error', error => resolve({ status: null, headers: {}, body: '', stderr: error.message }));
|
|
152
152
|
if (input !== undefined)
|
|
153
|
-
child.stdin
|
|
154
|
-
child.stdin
|
|
153
|
+
child.stdin?.write(input);
|
|
154
|
+
child.stdin?.end();
|
|
155
155
|
child.on('close', code => {
|
|
156
156
|
const response = parseGitHubCliHeaders(stdout, includeHeaders);
|
|
157
157
|
const failed = code !== 0;
|
|
@@ -231,9 +231,9 @@ export class GhApiClient {
|
|
|
231
231
|
}
|
|
232
232
|
async localLogin() {
|
|
233
233
|
return new Promise((resolve) => {
|
|
234
|
-
const child =
|
|
234
|
+
const child = spawnOwnedProcess('gh', ['auth', 'status', '--active', '--json', 'hosts'], { invocation: 'github.auth-status' });
|
|
235
235
|
let stdout = '';
|
|
236
|
-
child.stdout
|
|
236
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
237
237
|
child.on('error', () => resolve(null));
|
|
238
238
|
child.on('close', () => {
|
|
239
239
|
try {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn, spawnSync, } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { observability } from './observability.js';
|
|
5
|
+
const owned = new Map();
|
|
6
|
+
const byChild = new WeakMap();
|
|
7
|
+
let exitHookInstalled = false;
|
|
8
|
+
function audit(record, event, outcome, data) {
|
|
9
|
+
observability().record({
|
|
10
|
+
stage: 'daemon',
|
|
11
|
+
event,
|
|
12
|
+
outcome,
|
|
13
|
+
level: outcome === 'failed' ? 'warn' : 'info',
|
|
14
|
+
message: `${record.invocation} process ${event.replace('process.', '')}`,
|
|
15
|
+
correlation: record.correlation,
|
|
16
|
+
data: {
|
|
17
|
+
invocationId: record.invocationId,
|
|
18
|
+
invocation: record.invocation,
|
|
19
|
+
executable: basename(record.command),
|
|
20
|
+
pid: record.pid,
|
|
21
|
+
processGroup: record.processGroup,
|
|
22
|
+
elapsedMs: Date.now() - record.startedAt,
|
|
23
|
+
...data,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function signalOwned(record, signal) {
|
|
28
|
+
try {
|
|
29
|
+
if (record.processGroup !== null)
|
|
30
|
+
process.kill(-record.processGroup, signal);
|
|
31
|
+
else
|
|
32
|
+
record.child.kill(signal);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ESRCH')
|
|
37
|
+
return false;
|
|
38
|
+
audit(record, 'process.signal_failed', 'failed', { signal, error: error instanceof Error ? error.message : String(error) });
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function forceOwned(record, reason) {
|
|
43
|
+
if (!owned.has(record.pid))
|
|
44
|
+
return;
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
const result = spawnSync('taskkill', ['/pid', String(record.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
47
|
+
audit(record, 'process.signal', result.status === 0 ? 'succeeded' : 'failed', { reason, signal: 'taskkill-/t-/f', exitStatus: result.status });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const sent = signalOwned(record, 'SIGKILL');
|
|
51
|
+
audit(record, 'process.signal', sent ? 'succeeded' : 'failed', { reason, signal: 'SIGKILL' });
|
|
52
|
+
}
|
|
53
|
+
function installExitHook() {
|
|
54
|
+
if (exitHookInstalled)
|
|
55
|
+
return;
|
|
56
|
+
exitHookInstalled = true;
|
|
57
|
+
process.once('exit', () => {
|
|
58
|
+
for (const record of owned.values())
|
|
59
|
+
forceOwned(record, 'parent-shutdown');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function launchParentDeathSupervisor(pid, processGroup, graceMs) {
|
|
63
|
+
const posix = process.platform !== 'win32';
|
|
64
|
+
const script = posix ? [
|
|
65
|
+
"const pgid=Number(process.argv[1]),grace=Number(process.argv[2]);",
|
|
66
|
+
"const alive=()=>{try{process.kill(-pgid,0);return true}catch(e){return e&&e.code==='EPERM'}};",
|
|
67
|
+
"const stop=()=>{try{process.kill(-pgid,'SIGTERM')}catch{}setTimeout(()=>{try{process.kill(-pgid,'SIGKILL')}catch{}process.exit(0)},grace).unref()};",
|
|
68
|
+
"const timer=setInterval(()=>{if(!alive()){clearInterval(timer);process.exit(0);return}if(process.ppid===1){clearInterval(timer);stop()}},250);",
|
|
69
|
+
].join('') : [
|
|
70
|
+
"const {spawnSync}=require('node:child_process'),owner=Number(process.argv[1]),pid=Number(process.argv[2]);",
|
|
71
|
+
"const alive=p=>{try{process.kill(p,0);return true}catch{return false}};",
|
|
72
|
+
"const timer=setInterval(()=>{if(!alive(pid)){clearInterval(timer);process.exit(0);return}if(!alive(owner)){clearInterval(timer);spawnSync('taskkill',['/pid',String(pid),'/t','/f'],{stdio:'ignore'});process.exit(0)}},250);",
|
|
73
|
+
].join('');
|
|
74
|
+
const supervisorArgs = posix ? [String(processGroup), String(graceMs)] : [String(process.pid), String(pid)];
|
|
75
|
+
const supervisor = spawn(process.execPath, ['-e', script, ...supervisorArgs], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: 'ignore',
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
});
|
|
80
|
+
supervisor.unref();
|
|
81
|
+
return supervisor;
|
|
82
|
+
}
|
|
83
|
+
/** Spawn one disposable Relay-owned process group. Arguments are intentionally excluded from audit logs. */
|
|
84
|
+
export function spawnOwnedProcess(command, args, options) {
|
|
85
|
+
installExitHook();
|
|
86
|
+
if (options.signal?.aborted)
|
|
87
|
+
throw new Error(`${options.invocation} aborted before launch`);
|
|
88
|
+
const detached = process.platform !== 'win32';
|
|
89
|
+
const child = spawn(command, [...args], { ...options.spawn, detached });
|
|
90
|
+
if (!child.pid)
|
|
91
|
+
return child;
|
|
92
|
+
const record = {
|
|
93
|
+
child,
|
|
94
|
+
invocationId: randomUUID(),
|
|
95
|
+
invocation: options.invocation,
|
|
96
|
+
command,
|
|
97
|
+
pid: child.pid,
|
|
98
|
+
processGroup: detached ? child.pid : null,
|
|
99
|
+
startedAt: Date.now(),
|
|
100
|
+
graceMs: options.graceMs ?? 750,
|
|
101
|
+
correlation: options.correlation,
|
|
102
|
+
terminating: false,
|
|
103
|
+
};
|
|
104
|
+
owned.set(record.pid, record);
|
|
105
|
+
byChild.set(child, record);
|
|
106
|
+
record.supervisor = launchParentDeathSupervisor(record.pid, record.processGroup, record.graceMs);
|
|
107
|
+
audit(record, 'process.launched', 'started', {});
|
|
108
|
+
const abort = () => { void terminateOwnedProcess(child, 'abort'); };
|
|
109
|
+
record.abort = abort;
|
|
110
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
111
|
+
if (options.timeoutMs !== undefined) {
|
|
112
|
+
record.timeoutTimer = setTimeout(() => { void terminateOwnedProcess(child, 'timeout'); }, options.timeoutMs);
|
|
113
|
+
record.timeoutTimer.unref();
|
|
114
|
+
}
|
|
115
|
+
child.once('exit', (code, signal) => {
|
|
116
|
+
audit(record, 'process.exited', code === 0 ? 'succeeded' : 'failed', { exitStatus: code, signal });
|
|
117
|
+
// `close` waits for inherited stdio. End any descendants left behind by a
|
|
118
|
+
// wrapper before they can keep the caller alive indefinitely.
|
|
119
|
+
if (!record.terminating)
|
|
120
|
+
void terminateOwnedProcess(child, 'leader-exit');
|
|
121
|
+
});
|
|
122
|
+
child.once('close', () => {
|
|
123
|
+
if (record.timeoutTimer)
|
|
124
|
+
clearTimeout(record.timeoutTimer);
|
|
125
|
+
options.signal?.removeEventListener('abort', abort);
|
|
126
|
+
// The leader closing is not proof that its process group is gone.
|
|
127
|
+
// In particular, unref'd grandchildren need not inherit stdio.
|
|
128
|
+
// Preserve ownership and the force-kill timer through the grace window.
|
|
129
|
+
if (!record.terminating)
|
|
130
|
+
void terminateOwnedProcess(child, 'leader-exit');
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (record.forceTimer)
|
|
133
|
+
clearTimeout(record.forceTimer);
|
|
134
|
+
record.supervisor?.kill('SIGTERM');
|
|
135
|
+
owned.delete(record.pid);
|
|
136
|
+
}, record.graceMs + 100).unref();
|
|
137
|
+
});
|
|
138
|
+
return child;
|
|
139
|
+
}
|
|
140
|
+
export async function terminateOwnedProcess(child, reason, graceMs) {
|
|
141
|
+
const record = byChild.get(child);
|
|
142
|
+
if (!record || !owned.has(record.pid))
|
|
143
|
+
return;
|
|
144
|
+
if (!record.terminating) {
|
|
145
|
+
record.terminating = true;
|
|
146
|
+
const sent = signalOwned(record, 'SIGTERM');
|
|
147
|
+
audit(record, 'process.signal', sent ? 'succeeded' : 'failed', { reason, signal: 'SIGTERM' });
|
|
148
|
+
record.forceTimer = setTimeout(() => forceOwned(record, reason), graceMs ?? record.graceMs);
|
|
149
|
+
record.forceTimer.unref();
|
|
150
|
+
}
|
|
151
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
152
|
+
return;
|
|
153
|
+
await new Promise(resolve => {
|
|
154
|
+
const fallback = setTimeout(resolve, (graceMs ?? record.graceMs) + 1000);
|
|
155
|
+
fallback.unref();
|
|
156
|
+
child.once('close', () => { clearTimeout(fallback); resolve(); });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export async function shutdownOwnedProcesses(reason = 'parent-shutdown') {
|
|
160
|
+
await Promise.allSettled([...owned.values()].map(record => terminateOwnedProcess(record.child, reason)));
|
|
161
|
+
}
|
|
162
|
+
export function ownedProcessSnapshot() {
|
|
163
|
+
return [...owned.values()].map(record => ({
|
|
164
|
+
invocationId: record.invocationId,
|
|
165
|
+
invocation: record.invocation,
|
|
166
|
+
pid: record.pid,
|
|
167
|
+
processGroup: record.processGroup,
|
|
168
|
+
elapsedMs: Date.now() - record.startedAt,
|
|
169
|
+
}));
|
|
170
|
+
}
|
package/dist/planner.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { readFile } from 'node:fs/promises';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
@@ -7,6 +6,7 @@ import { executionContextPrompt } from './execution-context.js';
|
|
|
7
6
|
import { parseAgentGraph, validateAgentSelection } from './agent-graph.js';
|
|
8
7
|
import { dispatchPlannerTrigger } from './workflow/planner-trigger-v2.js';
|
|
9
8
|
import { createTaskEntity } from './workflow/entity-creation.js';
|
|
9
|
+
import { spawnOwnedProcess } from './owned-process.js';
|
|
10
10
|
export function semanticTaskTree(tasks) {
|
|
11
11
|
const work = tasks.filter(task => task.kind !== 'planner');
|
|
12
12
|
const ids = new Set(work.map(task => task.id));
|
|
@@ -384,11 +384,11 @@ export class CommandObjectivePlanner {
|
|
|
384
384
|
}
|
|
385
385
|
async plan(context, signal) {
|
|
386
386
|
return await new Promise((resolve, reject) => {
|
|
387
|
-
const child =
|
|
387
|
+
const child = spawnOwnedProcess(this.command, [], { invocation: 'planner.command', signal, correlation: { jobId: context.job.id }, spawn: { stdio: ['pipe', 'pipe', 'pipe'] } });
|
|
388
388
|
let stdout = '';
|
|
389
389
|
let stderr = '';
|
|
390
|
-
child.stdout
|
|
391
|
-
child.stderr
|
|
390
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
391
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
392
392
|
child.on('error', reject);
|
|
393
393
|
child.on('close', code => code === 0 ? (() => { try {
|
|
394
394
|
resolve(parsePlannerOutput(stdout));
|
|
@@ -396,8 +396,7 @@ export class CommandObjectivePlanner {
|
|
|
396
396
|
catch (error) {
|
|
397
397
|
reject(error);
|
|
398
398
|
} })() : reject(new Error(stderr.trim() || `Planner exited ${code}`)));
|
|
399
|
-
|
|
400
|
-
child.stdin.end(plannerInput(context));
|
|
399
|
+
child.stdin?.end(plannerInput(context));
|
|
401
400
|
});
|
|
402
401
|
}
|
|
403
402
|
}
|
|
@@ -430,7 +429,7 @@ export class AgentObjectivePlanner {
|
|
|
430
429
|
const resolvedContext = context.executionContext ? executionContextPrompt(context.executionContext) + '\n\n' : '';
|
|
431
430
|
const prompt = instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim() + '\n\n' + resolvedContext + '## Durable planner input\n\n' + plannerInput(context);
|
|
432
431
|
return await new Promise((resolve, reject) => {
|
|
433
|
-
const child =
|
|
432
|
+
const child = spawnOwnedProcess(this.command, ['exec', '--json', '--', prompt], { invocation: 'planner.agent', signal, correlation: { jobId: context.job.id }, spawn: { cwd: context.executionContext?.project.path, stdio: ['ignore', 'pipe', 'pipe'] } });
|
|
434
433
|
let stdoutBuffer = '';
|
|
435
434
|
let finalMessage = '';
|
|
436
435
|
let stderrTail = '';
|
|
@@ -439,14 +438,14 @@ export class AgentObjectivePlanner {
|
|
|
439
438
|
if (message)
|
|
440
439
|
finalMessage = message;
|
|
441
440
|
};
|
|
442
|
-
child.stdout
|
|
441
|
+
child.stdout?.on('data', (chunk) => {
|
|
443
442
|
stdoutBuffer += chunk.toString();
|
|
444
443
|
const lines = stdoutBuffer.split(/\r?\n/);
|
|
445
444
|
stdoutBuffer = lines.pop() ?? '';
|
|
446
445
|
for (const line of lines)
|
|
447
446
|
consume(line);
|
|
448
447
|
});
|
|
449
|
-
child.stderr
|
|
448
|
+
child.stderr?.on('data', (chunk) => { stderrTail = (stderrTail + chunk.toString()).slice(-16384); });
|
|
450
449
|
child.on('error', reject);
|
|
451
450
|
child.on('close', code => {
|
|
452
451
|
if (stdoutBuffer)
|
|
@@ -466,7 +465,6 @@ export class AgentObjectivePlanner {
|
|
|
466
465
|
reject(error);
|
|
467
466
|
}
|
|
468
467
|
});
|
|
469
|
-
signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true });
|
|
470
468
|
});
|
|
471
469
|
}
|
|
472
470
|
}
|
|
@@ -54,6 +54,7 @@ export class ReconcilerRuntimeBase {
|
|
|
54
54
|
continuationLive = new Set();
|
|
55
55
|
launchFailureClaims = new Set();
|
|
56
56
|
terminalWaiters = new Map();
|
|
57
|
+
startAckCleanups = new Set();
|
|
57
58
|
taskTransitions;
|
|
58
59
|
eventCoordinator;
|
|
59
60
|
detachedReconciles = new Set();
|
|
@@ -155,6 +156,7 @@ export class ReconcilerRuntimeBase {
|
|
|
155
156
|
await this.eventCoordinator.settle();
|
|
156
157
|
if (this.detachedReconciles.size > 0)
|
|
157
158
|
await Promise.allSettled([...this.detachedReconciles]);
|
|
159
|
+
await Promise.allSettled([...this.startAckCleanups].map(stop => stop()));
|
|
158
160
|
for (const live of this.live.values()) {
|
|
159
161
|
const sourceId = executionEventSource(live.jobId, live.taskId, live.executionId);
|
|
160
162
|
if (activeExecutionOwners.get(sourceId) === this)
|
|
@@ -204,9 +206,20 @@ export class ReconcilerRuntimeBase {
|
|
|
204
206
|
rejectAck(new StartAckTimeout(`Task ${taskId} worker_start_timeout: no worker task.started start-ack`));
|
|
205
207
|
}, timeoutMs);
|
|
206
208
|
let stopped = false;
|
|
207
|
-
const stop = async () => {
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
const stop = async () => {
|
|
210
|
+
if (stopped)
|
|
211
|
+
return;
|
|
212
|
+
stopped = true;
|
|
213
|
+
if (timer)
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
if (!settled) {
|
|
216
|
+
settled = true;
|
|
217
|
+
rejectAck(new StartAckTimeout(`Task ${taskId} worker_start_cancelled: start-ack owner closed`));
|
|
218
|
+
}
|
|
219
|
+
this.startAckCleanups.delete(stop);
|
|
220
|
+
await unsubscribe();
|
|
221
|
+
};
|
|
222
|
+
this.startAckCleanups.add(stop);
|
|
210
223
|
return { ack, stop };
|
|
211
224
|
}
|
|
212
225
|
registerExecutionOwner(jobId, taskId, executionId) {
|
|
@@ -325,7 +338,8 @@ export class ReconcilerRuntimeBase {
|
|
|
325
338
|
return;
|
|
326
339
|
if (task.adapter !== 'chatgpt')
|
|
327
340
|
this.clearTerminalTimer(taskId, executionId);
|
|
328
|
-
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', error ? 'failed' : 'succeeded',
|
|
341
|
+
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', live?.execution.processExit ? (live.execution.processExit.exitCode === 0 ? 'succeeded' : 'failed') : (error ? 'failed' : 'succeeded'), result?.summary ?? (error ? 'Worker runtime exited with error' : 'Worker runtime exited'), 'orchestrator', { authoritative: false, waitingForTerminalEvent: true, exitCode: live?.execution.processExit?.exitCode ?? null,
|
|
342
|
+
signal: live?.execution.processExit?.signal ?? null, stderr: live?.execution.processExit?.stderr ?? '', processError: error }, { agent: 'relay', component: 'agents-relay' }));
|
|
329
343
|
}
|
|
330
344
|
async timeoutTask(jobId, taskId, executionId) {
|
|
331
345
|
const live = this.live.get(taskId);
|
package/dist/relayd.js
CHANGED
|
@@ -26,6 +26,7 @@ import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
|
26
26
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
27
27
|
import { executionRouterRulesPath } from './routing-policy.js';
|
|
28
28
|
import { workerCandidates } from './worker-routing.js';
|
|
29
|
+
import { shutdownOwnedProcesses } from './owned-process.js';
|
|
29
30
|
import { routeExecution } from './execution-routing.js';
|
|
30
31
|
import { RelayObservability, classifyFailure, errorMessage, observability, setObservability } from './observability.js';
|
|
31
32
|
/** Initialize the process-wide structured observability sink once, from environment. */
|
|
@@ -508,7 +509,7 @@ export async function runDaemon(argv) {
|
|
|
508
509
|
const stop = () => { if (interval)
|
|
509
510
|
clearInterval(interval); if (scheduleInterval)
|
|
510
511
|
clearInterval(scheduleInterval); if (projectionInterval)
|
|
511
|
-
clearInterval(projectionInterval); void stopWatch().finally(() => { for (const store of relayStores.values())
|
|
512
|
+
clearInterval(projectionInterval); void Promise.allSettled([stopWatch(), shutdownOwnedProcesses('parent-shutdown')]).finally(() => { for (const store of relayStores.values())
|
|
512
513
|
store.close(); server.close(() => process.exit(0)); }); };
|
|
513
514
|
process.once('SIGINT', stop);
|
|
514
515
|
process.once('SIGTERM', stop);
|
package/dist/usage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import {
|
|
6
|
+
import { spawnOwnedProcess, terminateOwnedProcess } from './owned-process.js';
|
|
7
7
|
export class BrowserWorkspaceUsageReader {
|
|
8
8
|
command;
|
|
9
9
|
timeoutMs;
|
|
@@ -18,7 +18,7 @@ export class BrowserWorkspaceUsageReader {
|
|
|
18
18
|
async read() {
|
|
19
19
|
const observedAt = this.now().toISOString();
|
|
20
20
|
return await new Promise(resolve => {
|
|
21
|
-
const child =
|
|
21
|
+
const child = spawnOwnedProcess(this.command, [], { invocation: 'usage.browser-workspace', timeoutMs: this.timeoutMs, spawn: { stdio: ['pipe', 'pipe', 'ignore'] } });
|
|
22
22
|
let output = '';
|
|
23
23
|
let settled = false;
|
|
24
24
|
const finish = (capacity) => {
|
|
@@ -26,13 +26,13 @@ export class BrowserWorkspaceUsageReader {
|
|
|
26
26
|
return;
|
|
27
27
|
settled = true;
|
|
28
28
|
clearTimeout(timer);
|
|
29
|
-
child
|
|
29
|
+
void terminateOwnedProcess(child, 'cancel');
|
|
30
30
|
resolve(capacity);
|
|
31
31
|
};
|
|
32
32
|
const unavailable = (reason) => ({ name: 'Harness', poolSize: null, idle: null, leased: null, available: null, observedAt, reason });
|
|
33
33
|
const timer = setTimeout(() => finish(unavailable('Browser Workspace capacity probe timed out.')), this.timeoutMs);
|
|
34
|
-
child.stdout
|
|
35
|
-
child.stdout
|
|
34
|
+
child.stdout?.setEncoding('utf8');
|
|
35
|
+
child.stdout?.on('data', chunk => { output += chunk; });
|
|
36
36
|
child.once('error', () => finish(unavailable('Browser Workspace capacity probe could not be started.')));
|
|
37
37
|
child.once('close', code => {
|
|
38
38
|
if (settled)
|
|
@@ -65,7 +65,7 @@ export class BrowserWorkspaceUsageReader {
|
|
|
65
65
|
}
|
|
66
66
|
finish(unavailable(code === 0 ? 'Browser Workspace capacity probe returned no usable capacity.' : `Browser Workspace capacity probe exited with code ${code ?? 'unknown'}.`));
|
|
67
67
|
});
|
|
68
|
-
child.stdin
|
|
68
|
+
child.stdin?.end('import json\nprint(json.dumps(workspace_capacity()))\n');
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -133,9 +133,15 @@ export class CodexApiUsageReader {
|
|
|
133
133
|
async read() {
|
|
134
134
|
const generatedAt = this.now().toISOString();
|
|
135
135
|
return await new Promise(resolve => {
|
|
136
|
-
const child =
|
|
136
|
+
const child = spawnOwnedProcess('codex', ['app-server', '--stdio'], { invocation: 'usage.codex-app-server', timeoutMs: 10_000, spawn: { stdio: ['pipe', 'pipe', 'ignore'] } });
|
|
137
|
+
if (!child.stdout) {
|
|
138
|
+
resolve({ available: false, generatedAt, fiveHour: null, weekly: null, reason: 'Codex app-server stdout unavailable.' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
137
141
|
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
138
|
-
|
|
142
|
+
let queryTimer;
|
|
143
|
+
const finish = (snapshot) => { lines.close(); if (queryTimer)
|
|
144
|
+
clearTimeout(queryTimer); void terminateOwnedProcess(child, 'cancel'); resolve(snapshot); };
|
|
139
145
|
const timer = setTimeout(() => finish({ available: false, generatedAt, fiveHour: null, weekly: null, reason: 'Codex app-server rate-limit request timed out.' }), 10_000);
|
|
140
146
|
lines.on('line', line => {
|
|
141
147
|
try {
|
|
@@ -164,8 +170,8 @@ export class CodexApiUsageReader {
|
|
|
164
170
|
}
|
|
165
171
|
});
|
|
166
172
|
child.once('error', () => { clearTimeout(timer); finish({ available: false, generatedAt, fiveHour: null, weekly: null, reason: 'Codex app-server could not be started.' }); });
|
|
167
|
-
child.stdin
|
|
168
|
-
setTimeout(() => child.stdin
|
|
173
|
+
child.stdin?.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { clientInfo: { name: 'agents-relay', version: '1' }, capabilities: {} } }) + '\n');
|
|
174
|
+
queryTimer = setTimeout(() => child.stdin?.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'account/rateLimits/read', params: {} }) + '\n'), 100);
|
|
169
175
|
});
|
|
170
176
|
}
|
|
171
177
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { spawnOwnedProcess } from './owned-process.js';
|
|
5
|
+
async function gitOutput(args) {
|
|
6
|
+
return await new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawnOwnedProcess('git', args, { invocation: 'workspace.git', spawn: { stdio: ['ignore', 'pipe', 'pipe'] } });
|
|
8
|
+
let stdout = '';
|
|
9
|
+
let stderr = '';
|
|
10
|
+
child.stdout?.on('data', chunk => { stdout += chunk.toString(); });
|
|
11
|
+
child.stderr?.on('data', chunk => { stderr += chunk.toString(); });
|
|
12
|
+
child.once('error', reject);
|
|
13
|
+
child.once('close', code => code === 0 ? resolve(stdout) : reject(new Error(stderr.trim() || `git exited ${code}`)));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
7
16
|
export function defaultWorkspaceRoot() {
|
|
8
17
|
return join(homedir(), 'Workspace');
|
|
9
18
|
}
|
|
@@ -42,7 +51,7 @@ export async function discoverGitRepositories(root) {
|
|
|
42
51
|
}
|
|
43
52
|
async function origin(path) {
|
|
44
53
|
try {
|
|
45
|
-
return (await
|
|
54
|
+
return (await gitOutput(['-C', path, 'remote', 'get-url', 'origin'])).trim() || null;
|
|
46
55
|
}
|
|
47
56
|
catch {
|
|
48
57
|
return null;
|
|
@@ -111,7 +120,7 @@ export function taskUsesCanonicalProjectRun(task, tasks) {
|
|
|
111
120
|
return false;
|
|
112
121
|
}
|
|
113
122
|
async function git(path, args) {
|
|
114
|
-
return (await
|
|
123
|
+
return (await gitOutput(['-C', path, ...args])).trim();
|
|
115
124
|
}
|
|
116
125
|
export async function prepareManagedExecutionWorktree(projectPath, repository, prNumber, head, executionId) {
|
|
117
126
|
const primary = await git(projectPath, ['rev-parse', '--show-toplevel']);
|