agents-relay 1.0.18 → 1.0.20
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 +2 -2
- package/dist/adapters.js +21 -5
- package/dist/cli.js +12 -2
- package/dist/dashboard.js +13 -1
- package/dist/reconciler.js +34 -2
- package/dist/relayd.js +8 -2
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +1 -1
- package/skills/chatgpt-browser-worker/SKILL.md +8 -6
- package/skills/chatgpt-browser-worker/agents/browser-worker.agent.md +7 -5
- package/skills/chatgpt-browser-worker/scripts/_temporary_bh.py +6 -4
- package/skills/chatgpt-browser-worker/scripts/temporary_bh.py +1 -0
package/README.md
CHANGED
|
@@ -99,9 +99,9 @@ Managed tasks are descriptive work for agents, not shell commands. Use `codex` o
|
|
|
99
99
|
|
|
100
100
|
Every model-backed task declares a durable output with `--output task-pr` or `--output file --output-path PATH`. Agents Relay enriches the prompt with the PR URL, job/task identity, output contract, and mandatory terminal-event contract.
|
|
101
101
|
|
|
102
|
-
For Codex and ChatGPT, **events are authoritative task state**. Event publication uses the canonical Neo `events-bus` surface (`events__publish` for sandboxed/hosted workers, or `NEO_EVENTS_EMIT` for direct local workers); Agents Relay only subscribes and reconciles. Relay publishes `task.process.launched` only after successful adapter launch; `task.started` is worker-owned and must be a new, correlated start acknowledgement. Codex publishes `task.process.exited` when its synchronous runtime exits, while ChatGPT publishes `task.process.async_exited` after its submission runtime exits; neither substitutes for `task.completed`, `task.failed`, or `task.
|
|
102
|
+
For Codex and ChatGPT, **events are authoritative task state**. Event publication uses the canonical Neo `events-bus` surface (`events__publish` for sandboxed/hosted workers, or `NEO_EVENTS_EMIT` for direct local workers); Agents Relay only subscribes and reconciles. Relay publishes `task.process.launched` only after successful adapter launch; `task.started` is worker-owned and must be a new, correlated start acknowledgement. Codex publishes `task.process.exited` when its synchronous runtime exits, while ChatGPT publishes `task.process.async_exited` after its submission runtime exits; neither substitutes for `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. The ChatGPT worker-owned tab follows the configured close policy: `after-start` waits for the correlated start acknowledgement, `never` keeps it open for debug, and `after-terminal` waits for a new exact-task terminal event; start-ack failure closes only the owned tab after 60 seconds. The executing agent must publish exactly one correlated terminal event. Relay persists the matching durable state only from that terminal event. Model-backed tasks require an event bus and time out explicitly if no terminal event arrives.
|
|
103
103
|
|
|
104
|
-
The ChatGPT adapter uses the packaged `chatgpt-browser-worker` as a one-shot submitter. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance,
|
|
104
|
+
The ChatGPT adapter uses the packaged `chatgpt-browser-worker` as a one-shot submitter. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies `--chatgpt-tab-close-policy` (or `AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY`): `after-start` by default, `never` for test/debug, or `after-terminal` after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
|
|
105
105
|
|
|
106
106
|
~~~sh
|
|
107
107
|
npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
|
package/dist/adapters.js
CHANGED
|
@@ -98,6 +98,12 @@ export class CodexAdapter {
|
|
|
98
98
|
return execution;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
export function chatGptTabClosePolicy(value, fallback = 'after-start') {
|
|
102
|
+
const policy = value || fallback;
|
|
103
|
+
if (!['after-start', 'never', 'after-terminal'].includes(policy))
|
|
104
|
+
throw new Error(`Invalid ChatGPT tab close policy: ${policy}`);
|
|
105
|
+
return policy;
|
|
106
|
+
}
|
|
101
107
|
export class LaunchFailure extends Error {
|
|
102
108
|
}
|
|
103
109
|
export class StartAckTimeout extends LaunchFailure {
|
|
@@ -148,7 +154,7 @@ function parseDriverJson(output) {
|
|
|
148
154
|
function directBrowserWorkerRunner(root) {
|
|
149
155
|
return (_definition, request, _route, signal) => {
|
|
150
156
|
const releaseFile = join(process.env.TMPDIR ?? '/tmp', `agents-relay-chatgpt-release-${randomUUID()}`);
|
|
151
|
-
const args = [join(root, 'scripts', 'temporary_bh.py'), '--prompt', request.prompt, '--release-file', releaseFile];
|
|
157
|
+
const args = [join(root, 'scripts', 'temporary_bh.py'), '--prompt', request.prompt, '--release-file', releaseFile, '--close-policy', request.tabClosePolicy ?? 'after-start'];
|
|
152
158
|
for (const file of request.files ?? [])
|
|
153
159
|
args.push('--file', file);
|
|
154
160
|
const child = spawn('python3', args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -228,9 +234,11 @@ export class ChatGptAdapter {
|
|
|
228
234
|
capabilities = ['model', 'chatgpt', 'browser-harness'];
|
|
229
235
|
agentPath;
|
|
230
236
|
runner;
|
|
237
|
+
tabClosePolicy;
|
|
231
238
|
constructor(options = {}) {
|
|
232
239
|
this.agentPath = options.agentPath ?? process.env.AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT;
|
|
233
240
|
this.runner = options.agentRunner;
|
|
241
|
+
this.tabClosePolicy = chatGptTabClosePolicy(options.tabClosePolicy ?? process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY);
|
|
234
242
|
}
|
|
235
243
|
launch(task, signal, context = {}) {
|
|
236
244
|
if (!task.routing)
|
|
@@ -255,14 +263,21 @@ export class ChatGptAdapter {
|
|
|
255
263
|
const runtime = await loadBrowserWorkerRuntime(this.agentPath);
|
|
256
264
|
if (controller.signal.aborted)
|
|
257
265
|
throw new Error('browser-worker execution aborted');
|
|
258
|
-
const
|
|
266
|
+
const policy = chatGptTabClosePolicy(task.chatgptTabClosePolicy, this.tabClosePolicy);
|
|
267
|
+
const run = (this.runner ?? directBrowserWorkerRunner(runtime.root))(runtime.definition, { prompt: task.input, tabClosePolicy: policy }, route, controller.signal);
|
|
259
268
|
try {
|
|
260
269
|
const output = await (run.submitted ?? run.promise);
|
|
261
270
|
const response = parseBrowserWorkerResponse(output);
|
|
262
271
|
launchResolve();
|
|
263
|
-
if (context.waitForWorkerStart)
|
|
272
|
+
if (policy === 'after-start' && context.waitForWorkerStart)
|
|
264
273
|
await context.waitForWorkerStart;
|
|
265
|
-
|
|
274
|
+
else if (policy === 'after-terminal') {
|
|
275
|
+
if (!context.waitForTerminal)
|
|
276
|
+
throw new Error('ChatGPT after-terminal tab close policy requires a terminal event barrier');
|
|
277
|
+
await context.waitForTerminal;
|
|
278
|
+
}
|
|
279
|
+
if (policy !== 'never')
|
|
280
|
+
run.release?.();
|
|
266
281
|
if (run.submitted)
|
|
267
282
|
await run.promise;
|
|
268
283
|
return {
|
|
@@ -276,7 +291,8 @@ export class ChatGptAdapter {
|
|
|
276
291
|
}
|
|
277
292
|
catch (error) {
|
|
278
293
|
launchReject(error);
|
|
279
|
-
|
|
294
|
+
if (policy !== 'never')
|
|
295
|
+
run.release?.();
|
|
280
296
|
if (controller.signal.aborted)
|
|
281
297
|
throw error;
|
|
282
298
|
if (error instanceof StartAckTimeout)
|
package/dist/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ Actions:
|
|
|
90
90
|
'task update': `Update mutable durable task fields in place.
|
|
91
91
|
|
|
92
92
|
Required: --task-id ID and the normal job storage options.
|
|
93
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project.
|
|
93
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
|
|
94
94
|
Use --clear-routing to remove routing metadata. A model update requires --model; provider defaults to the existing provider or openai.
|
|
95
95
|
|
|
96
96
|
Example:
|
|
@@ -226,6 +226,12 @@ function priority(args, fallback = 'P2') { const value = arg(args, '--priority',
|
|
|
226
226
|
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
227
227
|
export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
|
|
228
228
|
throw new Error('--mode must be fixed or autonomous'); return value; }
|
|
229
|
+
export function chatGptTabClosePolicy(args) {
|
|
230
|
+
const value = arg(args, '--chatgpt-tab-close-policy', process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY || 'after-start');
|
|
231
|
+
if (!['after-start', 'never', 'after-terminal'].includes(value))
|
|
232
|
+
throw new Error('--chatgpt-tab-close-policy must be after-start, never, or after-terminal');
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
229
235
|
function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
230
236
|
function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
231
237
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
@@ -276,6 +282,7 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
|
276
282
|
if (outputKind === 'file' && !outputPath)
|
|
277
283
|
throw new Error('--output file requires --output-path');
|
|
278
284
|
const routing = { provider: arg(args, '--provider', 'openai'), model, profile: arg(args, '--profile') || undefined, reasoning: arg(args, '--reasoning') || undefined, cwd: arg(args, '--cwd') || undefined, projectId: arg(args, '--chatgpt-project') || undefined, decidedBy: 'cli', decidedAt: now };
|
|
285
|
+
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
279
286
|
return {
|
|
280
287
|
jobId: job.id,
|
|
281
288
|
id: arg(args, '--task-id', 'initial'),
|
|
@@ -286,6 +293,7 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
|
|
|
286
293
|
dependencies: arg(args, '--deps').split(',').filter(Boolean),
|
|
287
294
|
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
288
295
|
adapter,
|
|
296
|
+
chatgptTabClosePolicy: closePolicy,
|
|
289
297
|
input: input || 'true',
|
|
290
298
|
output: outputKind === 'file' ? { kind: 'file', path: outputPath } : { kind: 'task_pr' },
|
|
291
299
|
routing,
|
|
@@ -337,6 +345,8 @@ async function runTaskCommand(action, args) {
|
|
|
337
345
|
task.dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
338
346
|
if (hasArg(args, '--capabilities'))
|
|
339
347
|
task.capabilities = arg(args, '--capabilities').split(',').filter(Boolean);
|
|
348
|
+
if (hasArg(args, '--chatgpt-tab-close-policy'))
|
|
349
|
+
task.chatgptTabClosePolicy = chatGptTabClosePolicy(args);
|
|
340
350
|
task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
|
|
341
351
|
task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
|
|
342
352
|
if (hasArg(args, '--max-attempts') && task.maxAttempts < task.attempt)
|
|
@@ -414,7 +424,7 @@ async function runJobCommand(action, args) {
|
|
|
414
424
|
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state, task: start ? arg(args, '--task-id', 'initial') : undefined }, null, 2));
|
|
415
425
|
}
|
|
416
426
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
417
|
-
export function runtime(store, args, bus) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, eventBus: bus, emit }); }
|
|
427
|
+
export function runtime(store, args, bus) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, eventBus: bus, emit, chatgptTabClosePolicy: policy }); }
|
|
418
428
|
export async function createService(args) {
|
|
419
429
|
const loaded = await storeFor(args);
|
|
420
430
|
const upstream = eventBus(args);
|
package/dist/dashboard.js
CHANGED
|
@@ -354,6 +354,12 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
354
354
|
throw new Error(`Managed job ${requestedJobId ?? ''} not found`);
|
|
355
355
|
return { id: jobId, json: JSON.stringify(await store.load(jobId)) };
|
|
356
356
|
};
|
|
357
|
+
const broadcastSnapshots = async (jobIds = []) => {
|
|
358
|
+
const snapshots = await Promise.all((jobIds.length > 0 ? jobIds : [undefined]).map(requestedJobId => snapshot(requestedJobId)));
|
|
359
|
+
for (const value of snapshots)
|
|
360
|
+
for (const client of clients)
|
|
361
|
+
client.write(`data: ${value.json}\n\n`);
|
|
362
|
+
};
|
|
357
363
|
const usage = async () => {
|
|
358
364
|
if (!usageRegistry)
|
|
359
365
|
throw new Error('Account usage telemetry is unavailable');
|
|
@@ -394,7 +400,13 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
394
400
|
}
|
|
395
401
|
res.writeHead(202);
|
|
396
402
|
res.end('Accepted');
|
|
397
|
-
queueMicrotask(() => {
|
|
403
|
+
queueMicrotask(() => {
|
|
404
|
+
void (webhook.refresh ? webhook.refresh(event) : Promise.resolve([]))
|
|
405
|
+
.then(jobIds => broadcastSnapshots(jobIds))
|
|
406
|
+
.catch(error => process.stderr.write(`warning: webhook dashboard snapshot unavailable: ${error instanceof Error ? error.message : String(error)}\n`))
|
|
407
|
+
.then(() => webhook.wake(event))
|
|
408
|
+
.catch(error => process.stderr.write(`warning: webhook wake unavailable: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
409
|
+
});
|
|
398
410
|
return;
|
|
399
411
|
}
|
|
400
412
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/jobs') {
|
package/dist/reconciler.js
CHANGED
|
@@ -46,13 +46,14 @@ export class Reconciler {
|
|
|
46
46
|
plannerLive = new Map();
|
|
47
47
|
terminalTimers = new Map();
|
|
48
48
|
continuationLive = new Set();
|
|
49
|
+
terminalWaiters = new Map();
|
|
49
50
|
reconciling = false;
|
|
50
51
|
constructor(store, options) {
|
|
51
52
|
this.store = store;
|
|
52
53
|
this.options = options;
|
|
53
54
|
}
|
|
54
55
|
async handleEvent(event) {
|
|
55
|
-
if (['task.completed', 'task.failed', 'task.blocked'].includes(event.type)) {
|
|
56
|
+
if (['task.completed', 'task.failed', 'task.blocked', 'task.cancelled'].includes(event.type)) {
|
|
56
57
|
await this.applyTerminalEvent(event);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
@@ -306,12 +307,20 @@ export class Reconciler {
|
|
|
306
307
|
let execution;
|
|
307
308
|
const workerTask = { ...task, input: workerInput };
|
|
308
309
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, Date.now()) : null;
|
|
309
|
-
const
|
|
310
|
+
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
311
|
+
const terminalWait = task.adapter === 'chatgpt' && closePolicy === 'after-terminal'
|
|
312
|
+
? this.terminalWait(task.id, durableExecutionId, startedAt - 1)
|
|
313
|
+
: null;
|
|
314
|
+
const launchContext = {
|
|
315
|
+
...(startAckSetup ? { waitForWorkerStart: startAckSetup.ack } : {}),
|
|
316
|
+
...(terminalWait ? { waitForTerminal: terminalWait.promise } : {}),
|
|
317
|
+
};
|
|
310
318
|
try {
|
|
311
319
|
execution = adapter.launch(workerTask, controller.signal, launchContext);
|
|
312
320
|
}
|
|
313
321
|
catch (error) {
|
|
314
322
|
await startAckSetup?.stop();
|
|
323
|
+
terminalWait?.cancel();
|
|
315
324
|
await this.failLaunch(job.id, task.id, task.executionId, error instanceof Error ? error.message : String(error));
|
|
316
325
|
return;
|
|
317
326
|
}
|
|
@@ -502,10 +511,15 @@ export class Reconciler {
|
|
|
502
511
|
const task = job.tasks.find(item => item.id === event.task_id);
|
|
503
512
|
if (!task || !['codex', 'chatgpt'].includes(task.adapter) || task.state !== 'RUNNING')
|
|
504
513
|
return;
|
|
514
|
+
const terminalWaiter = this.terminalWaiters.get(task.id);
|
|
515
|
+
if (terminalWaiter && (!event.timestamp || new Date(event.timestamp).getTime() < terminalWaiter.launchBarrier))
|
|
516
|
+
return;
|
|
505
517
|
this.clearExecution(task.id, task.executionId);
|
|
506
518
|
task.leaseOwner = null;
|
|
507
519
|
task.leaseExpiresAt = null;
|
|
508
520
|
task.updatedAt = event.timestamp || new Date().toISOString();
|
|
521
|
+
this.terminalWaiters.get(task.id)?.resolve();
|
|
522
|
+
this.terminalWaiters.delete(task.id);
|
|
509
523
|
if (event.type === 'task.completed') {
|
|
510
524
|
task.result = { summary: event.message || `Task ${task.id} completed`, data: event.data };
|
|
511
525
|
task.error = null;
|
|
@@ -523,8 +537,26 @@ export class Reconciler {
|
|
|
523
537
|
transitionTask(task, 'BLOCKED');
|
|
524
538
|
await this.store.saveTask(task);
|
|
525
539
|
}
|
|
540
|
+
else if (event.type === 'task.cancelled') {
|
|
541
|
+
task.error = event.message || `Task ${task.id} cancelled`;
|
|
542
|
+
transitionTask(task, 'CANCELLED');
|
|
543
|
+
await this.store.saveTask(task);
|
|
544
|
+
}
|
|
526
545
|
this.detach(this.reconcile(job.id), job.id, task.id, 'terminal-event reconciliation');
|
|
527
546
|
}
|
|
547
|
+
terminalWait(taskId, executionId, launchBarrier) {
|
|
548
|
+
let resolve;
|
|
549
|
+
const promise = new Promise(resolver => { resolve = resolver; });
|
|
550
|
+
this.terminalWaiters.set(taskId, { executionId, launchBarrier, resolve });
|
|
551
|
+
return {
|
|
552
|
+
promise,
|
|
553
|
+
cancel: () => {
|
|
554
|
+
const current = this.terminalWaiters.get(taskId);
|
|
555
|
+
if (current?.executionId === executionId)
|
|
556
|
+
this.terminalWaiters.delete(taskId);
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
528
560
|
async deliverContinuation(job, task) {
|
|
529
561
|
const continuation = task.continuation ?? job.continuation;
|
|
530
562
|
if (!continuation || task.continuationDeliveredAt || this.continuationLive.has(task.id))
|
package/dist/relayd.js
CHANGED
|
@@ -121,16 +121,22 @@ export async function runDaemon(argv) {
|
|
|
121
121
|
const webhook = secret ? {
|
|
122
122
|
secret,
|
|
123
123
|
repositories: new Set(repositories),
|
|
124
|
+
refresh: async (event) => {
|
|
125
|
+
if (!event.repository || !event.pullRequest)
|
|
126
|
+
return [];
|
|
127
|
+
const refreshed = await refreshPullRequest(event.repository, event.pullRequest);
|
|
128
|
+
return refreshed.map(item => item.job.id);
|
|
129
|
+
},
|
|
124
130
|
wake: async (event) => {
|
|
125
131
|
if (!event.repository || !event.pullRequest)
|
|
126
132
|
return;
|
|
127
133
|
try {
|
|
128
|
-
const refreshed =
|
|
134
|
+
const refreshed = cachedOverviews.filter(item => item.job.repository === event.repository && item.job.prNumber === event.pullRequest);
|
|
129
135
|
for (const item of refreshed)
|
|
130
136
|
await pool.webhookWake(bus, item.job, event.deliveryId, event.event, event.action);
|
|
131
137
|
}
|
|
132
138
|
catch (error) {
|
|
133
|
-
process.stderr.write(`warning: webhook
|
|
139
|
+
process.stderr.write(`warning: webhook wake unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
142
|
} : undefined;
|
package/dist/store.js
CHANGED
|
@@ -83,7 +83,7 @@ function immutableTaskDefinition(task) {
|
|
|
83
83
|
return {
|
|
84
84
|
jobId: task.jobId, id: task.id, priority: task.priority, projectName: task.projectName, agentName: task.agentName,
|
|
85
85
|
kind: task.kind ?? 'work', parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, adapter: task.adapter,
|
|
86
|
-
input: task.input, output: task.output, routing, continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
86
|
+
input: task.input, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
function sameTaskDefinition(left, right) {
|
package/package.json
CHANGED
|
@@ -78,7 +78,7 @@ The job/task descriptions remain the durable source of intent. Adapters do not i
|
|
|
78
78
|
|
|
79
79
|
## ChatGPT worker
|
|
80
80
|
|
|
81
|
-
The ChatGPT adapter uses the packaged `chatgpt-browser-worker` one-shot path. It opens a fresh Temporary Chat tab, submits the complete task with the account defaults, verifies acceptance,
|
|
81
|
+
The ChatGPT adapter uses the packaged `chatgpt-browser-worker` one-shot path. It opens a fresh Temporary Chat tab, submits the complete task with the account defaults, verifies acceptance, applies the configured `after-start` (default), `never`, or `after-terminal` owned-tab close policy, and returns a submission receipt.
|
|
82
82
|
|
|
83
83
|
ChatGPT thread identity is diagnostic only. It is not persisted as a resumable task handle, and Relay does not poll, resume, retrieve a result from, or delete a ChatGPT conversation as part of task lifecycle.
|
|
84
84
|
|
|
@@ -48,12 +48,14 @@ Neo event contract. Events are execution observability, not a third output mode.
|
|
|
48
48
|
## Runtime behavior
|
|
49
49
|
|
|
50
50
|
The worker is one-shot. It opens an isolated worker-owned ChatGPT tab, submits
|
|
51
|
-
the complete task, verifies that ChatGPT accepted the submission,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
the complete task, verifies that ChatGPT accepted the submission, and applies
|
|
52
|
+
the configured tab-close policy. `after-start` waits for the exact
|
|
53
|
+
post-submission worker `task.started` acknowledgement, `never` leaves the owned
|
|
54
|
+
tab open for debugging, and `after-terminal` waits for a new exact-task
|
|
55
|
+
`task.completed`, `task.failed`, `task.blocked`, or `task.cancelled` event.
|
|
56
|
+
ChatGPT continues the task independently and publishes the result through the
|
|
57
|
+
declared output contract. If the start acknowledgement does not arrive within
|
|
58
|
+
60 seconds, close only the owned tab and fail with `worker_start_timeout`.
|
|
57
59
|
|
|
58
60
|
The worker must never interact through a pre-existing user ChatGPT tab.
|
|
59
61
|
Browser details, transient conversation identity, submission verification,
|
|
@@ -28,11 +28,13 @@ If the output declaration is missing or ambiguous, do not invent one.
|
|
|
28
28
|
settings. Do not change model or thinking settings.
|
|
29
29
|
- Upload requested files, submit the complete task prompt, and verify that the
|
|
30
30
|
submission became a new user turn.
|
|
31
|
-
- Once submission is verified,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
- Once submission is verified, follow the configured owned-tab close policy:
|
|
32
|
+
`after-start` closes after a new worker-owned `task.started` event, `never`
|
|
33
|
+
leaves the owned tab open for test/debug use, and `after-terminal` closes only
|
|
34
|
+
after a new exact-task `task.completed`, `task.failed`, `task.blocked`, or
|
|
35
|
+
`task.cancelled` event. Never close a user tab. Do not wait for the assistant
|
|
36
|
+
response or reopen/poll the conversation. If the start acknowledgement does
|
|
37
|
+
not arrive within 60 seconds, close only the owned tab and report a timeout.
|
|
36
38
|
- Treat any observed conversation/thread identity only as diagnostic evidence,
|
|
37
39
|
never as a resumable handle.
|
|
38
40
|
- Progress and terminal success/failure for the actual delegated task are
|
|
@@ -9,6 +9,7 @@ from browser_harness import *
|
|
|
9
9
|
|
|
10
10
|
CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
|
|
11
11
|
_OWNED_TABS = []
|
|
12
|
+
_KEEP_OWNED_TAB_OPEN = CFG.get("close_policy", "after-start") == "never"
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def _new_owned_tab(url):
|
|
@@ -21,6 +22,8 @@ def _new_owned_tab(url):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _close_owned_tabs():
|
|
25
|
+
if _KEEP_OWNED_TAB_OPEN:
|
|
26
|
+
return
|
|
24
27
|
while _OWNED_TABS:
|
|
25
28
|
try:
|
|
26
29
|
close_tab(_OWNED_TABS.pop())
|
|
@@ -185,8 +188,7 @@ print(json.dumps({
|
|
|
185
188
|
release_file = CFG.get("release_file")
|
|
186
189
|
if not release_file:
|
|
187
190
|
raise RuntimeError("worker release file was not configured")
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
if CFG.get("close_policy", "after-start") == "never":
|
|
192
|
+
raise SystemExit(0)
|
|
193
|
+
while not os.path.exists(release_file):
|
|
190
194
|
time.sleep(.1)
|
|
191
|
-
if not os.path.exists(release_file):
|
|
192
|
-
raise RuntimeError("worker start acknowledgement release was not observed")
|
|
@@ -17,6 +17,7 @@ def main() -> int:
|
|
|
17
17
|
parser.add_argument("--prompt", required=True)
|
|
18
18
|
parser.add_argument("--file", action="append", default=[])
|
|
19
19
|
parser.add_argument("--release-file", required=True)
|
|
20
|
+
parser.add_argument("--close-policy", choices=["after-start", "never", "after-terminal"], default="after-start")
|
|
20
21
|
args = parser.parse_args()
|
|
21
22
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as handle:
|
|
22
23
|
json.dump(vars(args), handle, ensure_ascii=False)
|