agents-relay 2.0.58 → 2.0.60
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-shutdown.js +16 -0
- package/dist/cli-support.js +14 -1
- package/dist/cli.js +45 -18
- package/dist/continuation.js +3 -3
- package/dist/dashboard.js +2 -0
- package/dist/decision.js +53 -5
- package/dist/execution-process-tracker.js +35 -0
- package/dist/execution-recovery.js +7 -2
- package/dist/execution-routing.js +40 -8
- package/dist/github-client.js +8 -8
- package/dist/model-redecision.js +1 -1
- package/dist/owned-process.js +170 -0
- package/dist/planner-runtime.js +3 -1
- package/dist/planner.js +11 -11
- package/dist/process-identity.js +83 -0
- package/dist/projected-store.js +8 -0
- package/dist/reconciler-review.js +52 -15
- package/dist/reconciler-runtime.js +25 -6
- package/dist/reconciler.js +1 -1
- package/dist/relay-store.js +33 -0
- package/dist/relayd.js +9 -5
- package/dist/task-update.js +52 -0
- package/dist/terminal-event-watchdog.js +11 -0
- package/dist/usage.js +16 -10
- package/dist/workspace.js +14 -5
- package/package.json +1 -1
- package/workflow/task-lifecycle.mmd +1 -0
package/dist/relayd.js
CHANGED
|
@@ -17,15 +17,17 @@ import { eventBusResolutionMessage, publishOrchestratorTaskStarted, publishOrche
|
|
|
17
17
|
import { eventFor } from './events.js';
|
|
18
18
|
import { applyJobActivateAction } from './job-actions.js';
|
|
19
19
|
import { applyOrchestratorStartAction, applyOrchestratorCompleteAction } from './task-actions.js';
|
|
20
|
+
import { managedTaskUpdateHandler } from './task-update.js';
|
|
20
21
|
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
21
22
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
22
23
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
23
24
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
24
|
-
import { decisionMode,
|
|
25
|
+
import { decisionMode, executionDecisionEngine } from './decision.js';
|
|
25
26
|
import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
26
27
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
27
28
|
import { executionRouterRulesPath } from './routing-policy.js';
|
|
28
29
|
import { workerCandidates } from './worker-routing.js';
|
|
30
|
+
import { shutdownOwnedProcesses } from './owned-process.js';
|
|
29
31
|
import { routeExecution } from './execution-routing.js';
|
|
30
32
|
import { RelayObservability, classifyFailure, errorMessage, observability, setObservability } from './observability.js';
|
|
31
33
|
/** Initialize the process-wide structured observability sink once, from environment. */
|
|
@@ -196,7 +198,7 @@ export async function runDaemon(argv) {
|
|
|
196
198
|
job, githubState: snapshot.facts.state, draft: false, mergedAt: null, closedAt: null, publication: null,
|
|
197
199
|
}));
|
|
198
200
|
}));
|
|
199
|
-
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine:
|
|
201
|
+
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: async (job) => resolveRepositoryProjectPath(repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot), job.projectPath) });
|
|
200
202
|
const refreshAll = async (full = false) => {
|
|
201
203
|
if (client.rateLimitStatus().limited)
|
|
202
204
|
return cachedOverviews;
|
|
@@ -403,6 +405,8 @@ export async function runDaemon(argv) {
|
|
|
403
405
|
cancel: workspaceJobAction('cancel'),
|
|
404
406
|
jobSetState: workspaceJobSetState,
|
|
405
407
|
taskCreate: workspaceTaskCreate,
|
|
408
|
+
taskUpdate: managedTaskUpdateHandler(jobId => { const row = cachedOverviews.find(item => item.job.id === jobId); if (!row)
|
|
409
|
+
throw new Error('Job not found'); return { store: runtimeStore(row.job.repository, row.job.prNumber), repository: row.job.repository, prNumber: row.job.prNumber }; }, (repository, prNumber, jobId) => pool.reconcileTarget(repository, prNumber, jobId, 'control.task.update')),
|
|
406
410
|
taskStart: workspaceTaskStart,
|
|
407
411
|
taskRetry: workspaceTaskAction('retry'),
|
|
408
412
|
taskCancel: workspaceTaskAction('cancel'),
|
|
@@ -423,7 +427,7 @@ export async function runDaemon(argv) {
|
|
|
423
427
|
{ name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-workspace'] },
|
|
424
428
|
], [], usage);
|
|
425
429
|
const result = await routeExecution(workers, task.workerRequirements ?? { capabilities: task.capabilities ?? [] }, {
|
|
426
|
-
decisionEngine:
|
|
430
|
+
decisionEngine: executionDecisionEngine(),
|
|
427
431
|
decisionMode: 'active',
|
|
428
432
|
taskFacts: {
|
|
429
433
|
job: {
|
|
@@ -436,7 +440,7 @@ export async function runDaemon(argv) {
|
|
|
436
440
|
id: task.id, kind: task.kind ?? 'work', storySize: task.storySize ?? 'M',
|
|
437
441
|
priority: task.priority ?? null, agentName: task.agentName ?? null,
|
|
438
442
|
parentTaskId: task.parentTaskId, dependencies: task.dependencies,
|
|
439
|
-
capabilities: task.capabilities, input: task.input,
|
|
443
|
+
capabilities: task.capabilities, taskCapabilities: task.taskCapabilities, workerCapabilities: task.workerCapabilities, input: task.input,
|
|
440
444
|
attachments: task.attachments ?? [], output: task.output ?? null,
|
|
441
445
|
state: task.state, attempt: task.attempt, maxAttempts: task.maxAttempts,
|
|
442
446
|
},
|
|
@@ -508,7 +512,7 @@ export async function runDaemon(argv) {
|
|
|
508
512
|
const stop = () => { if (interval)
|
|
509
513
|
clearInterval(interval); if (scheduleInterval)
|
|
510
514
|
clearInterval(scheduleInterval); if (projectionInterval)
|
|
511
|
-
clearInterval(projectionInterval); void stopWatch().finally(() => { for (const store of relayStores.values())
|
|
515
|
+
clearInterval(projectionInterval); void Promise.allSettled([stopWatch(), shutdownOwnedProcesses('parent-shutdown')]).finally(() => { for (const store of relayStores.values())
|
|
512
516
|
store.close(); server.close(() => process.exit(0)); }); };
|
|
513
517
|
process.once('SIGINT', stop);
|
|
514
518
|
process.once('SIGTERM', stop);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { applyTaskHoldAction, applyTaskQueueAction } from './task-actions.js';
|
|
2
|
+
const editableFields = [
|
|
3
|
+
'priority', 'storySize', 'projectName', 'agentName', 'agentGraph',
|
|
4
|
+
'parentTaskId', 'dependencies', 'capabilities', 'workerRequirements',
|
|
5
|
+
'adapter', 'chatgptTabClosePolicy', 'input', 'attachments',
|
|
6
|
+
'routing', 'timeoutMs', 'maxAttempts',
|
|
7
|
+
];
|
|
8
|
+
/** All managed CLI Task updates commit through the daemon's operational store. */
|
|
9
|
+
export async function applyManagedTaskUpdate(store, jobId, taskId, proposed, expectedUpdatedAt) {
|
|
10
|
+
const job = await store.load(jobId);
|
|
11
|
+
const current = job.tasks.find(item => item.id === taskId);
|
|
12
|
+
if (!current || proposed.id !== taskId || proposed.jobId !== jobId)
|
|
13
|
+
throw new Error('Task update identity mismatch');
|
|
14
|
+
if (current.updatedAt !== expectedUpdatedAt)
|
|
15
|
+
throw new Error('Task changed since update preflight; refresh and retry');
|
|
16
|
+
if (['RUNNING', 'UNBLOCKED', 'SUCCEEDED', 'CANCELLED'].includes(current.state) || current.executionId || current.leaseOwner)
|
|
17
|
+
throw new Error('Task is not mutable while active or terminal');
|
|
18
|
+
if (current.attempt > 0 && current.adapter !== proposed.adapter)
|
|
19
|
+
throw new Error('Cannot replace adapter after an execution attempt');
|
|
20
|
+
if (proposed.maxAttempts < current.attempt)
|
|
21
|
+
throw new Error('Retry budget is below execution attempts');
|
|
22
|
+
const next = structuredClone(current);
|
|
23
|
+
for (const key of editableFields) {
|
|
24
|
+
if (Object.hasOwn(proposed, key))
|
|
25
|
+
next[key] = structuredClone(proposed[key]);
|
|
26
|
+
else
|
|
27
|
+
delete next[key];
|
|
28
|
+
}
|
|
29
|
+
if (next.dependencies.includes(taskId))
|
|
30
|
+
throw new Error('Task cannot depend on itself');
|
|
31
|
+
if (proposed.state !== current.state) {
|
|
32
|
+
if (proposed.state === 'DRAFT' && current.state === 'QUEUED')
|
|
33
|
+
await applyTaskHoldAction(next);
|
|
34
|
+
else if (proposed.state === 'QUEUED')
|
|
35
|
+
await applyTaskQueueAction(job, next);
|
|
36
|
+
else
|
|
37
|
+
throw new Error('Task state change requires a dedicated lifecycle action');
|
|
38
|
+
}
|
|
39
|
+
if (next.adapter !== current.adapter || JSON.stringify(next.routing) !== JSON.stringify(current.routing))
|
|
40
|
+
next.workerSelection = undefined;
|
|
41
|
+
next.updatedAt = new Date().toISOString();
|
|
42
|
+
await store.saveTask(next);
|
|
43
|
+
return store.load(jobId);
|
|
44
|
+
}
|
|
45
|
+
export function managedTaskUpdateHandler(locate, wake) {
|
|
46
|
+
return async (jobId, taskId, task, expectedUpdatedAt) => {
|
|
47
|
+
const target = locate(jobId);
|
|
48
|
+
const updated = await applyManagedTaskUpdate(target.store, jobId, taskId, task, expectedUpdatedAt);
|
|
49
|
+
await wake(target.repository, target.prNumber, jobId);
|
|
50
|
+
return updated;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace the overall worker deadline with a shorter post-exit deadline.
|
|
3
|
+
* Execution outcomes remain owned by the authoritative task lifecycle graph.
|
|
4
|
+
* Never extend the original overall timeout.
|
|
5
|
+
*/
|
|
6
|
+
export function rearmTerminalEventDeadline(timers, taskId, overallRemainingMs, graceMs, expire) {
|
|
7
|
+
const existing = timers.get(taskId);
|
|
8
|
+
if (existing)
|
|
9
|
+
clearTimeout(existing);
|
|
10
|
+
timers.set(taskId, setTimeout(expire, Math.max(1, Math.min(overallRemainingMs, graceMs))));
|
|
11
|
+
}
|
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']);
|
package/package.json
CHANGED
|
@@ -120,6 +120,7 @@ queued -->|on: worker.delivery_failed| failed
|
|
|
120
120
|
queued -->|on: worker.start_ack_failed| failed
|
|
121
121
|
queued -->|on: planner.unavailable| plannerUnavailableMark
|
|
122
122
|
plannerUnavailableMark --> failed
|
|
123
|
+
queued -->|on: execution.orphaned| orphanRetryAvailable
|
|
123
124
|
queued -->|on: lease.expired| retryAvailableQueued
|
|
124
125
|
retryAvailableQueued -->|true| leaseRetryQueuedClearError
|
|
125
126
|
leaseRetryQueuedClearError --> leaseRetryQueuedClearLease
|