agents-relay 2.0.42 → 2.0.44
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 +1 -1
- package/dist/adapters.js +7 -38
- package/dist/cli-blocker.js +2 -2
- package/dist/cli-support.js +9 -0
- package/dist/cli-unblock.js +15 -12
- package/dist/cli.js +21 -28
- package/package.json +2 -2
- package/skills/agents-relay/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ Every model-backed task declares a durable output with `--output task-pr` or `--
|
|
|
227
227
|
|
|
228
228
|
For Codex and ChatGPT, **correlated worker events are authoritative lifecycle inputs**, not a separate durable store. 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 subscribes, validates correlation, applies graph transitions, persists results in RelayStore, and projects them to GitHub. 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.
|
|
229
229
|
|
|
230
|
-
The ChatGPT adapter uses
|
|
230
|
+
The ChatGPT adapter uses Relay's `agents/chatgpt-worker.agent.md` lifecycle contract and Browser Workspace `platforms/chatgpt` for all ChatGPT browser mechanics. 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.
|
|
231
231
|
|
|
232
232
|
~~~sh
|
|
233
233
|
npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
|
package/dist/adapters.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
export function buildCodexArgs(route, input) {
|
|
6
6
|
const args = ['exec', '--json'];
|
|
@@ -132,24 +132,6 @@ export class LaunchFailure extends Error {
|
|
|
132
132
|
}
|
|
133
133
|
export class StartAckTimeout extends LaunchFailure {
|
|
134
134
|
}
|
|
135
|
-
async function loadBrowserWorkerRuntime(context, explicit) {
|
|
136
|
-
const resolvedSkill = context?.skills.find(skill => skill.name === 'chatgpt-browser-worker');
|
|
137
|
-
// Once a worker receives a resolved execution context, that context owns
|
|
138
|
-
// skill discovery. The explicit path remains the fallback for direct
|
|
139
|
-
// adapter use, where no shared context has been resolved yet.
|
|
140
|
-
const candidate = resolvedSkill
|
|
141
|
-
? join(dirname(resolvedSkill.path), 'agents', 'browser-worker.agent.md')
|
|
142
|
-
: explicit;
|
|
143
|
-
if (!candidate)
|
|
144
|
-
throw new Error('chatgpt-browser-worker is not visible in the resolved execution context');
|
|
145
|
-
try {
|
|
146
|
-
await access(candidate);
|
|
147
|
-
return { definition: await readFile(candidate, 'utf8'), root: dirname(dirname(candidate)) };
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
throw new Error(`chatgpt-browser-worker agent definition not found at resolved path: ${candidate}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
135
|
function parseDriverJson(output) {
|
|
154
136
|
const lines = output.trim().split(/\r?\n/).filter(Boolean);
|
|
155
137
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
@@ -200,7 +182,7 @@ function parseWorkspaceJson(output) {
|
|
|
200
182
|
export async function ensureChatGptBrowserWorkspace(command = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND) {
|
|
201
183
|
await browserWorkspaceCommand(browserWorkspaceCli(command), ['create', CHATGPT_BROWSER_WORKSPACE, String(CHATGPT_BROWSER_WORKSPACE_POOL_SIZE)]);
|
|
202
184
|
}
|
|
203
|
-
function sessionBrowserWorkerRunner(
|
|
185
|
+
function sessionBrowserWorkerRunner() {
|
|
204
186
|
const cli = browserWorkspaceCli();
|
|
205
187
|
return (_definition, request, _route, signal) => {
|
|
206
188
|
const policy = request.tabClosePolicy ?? 'after-start';
|
|
@@ -223,16 +205,10 @@ function sessionBrowserWorkerRunner(root) {
|
|
|
223
205
|
const promise = (async () => {
|
|
224
206
|
const configFile = join(process.env.TMPDIR ?? '/tmp', `agents-relay-chatgpt-${randomUUID()}.json`);
|
|
225
207
|
try {
|
|
226
|
-
const [readiness, submitSource, temporarySource] = await Promise.all([
|
|
227
|
-
readFile(join(root, 'scripts', '_submit_readiness.py'), 'utf8'),
|
|
228
|
-
readFile(join(root, 'scripts', '_submit_bh.py'), 'utf8'),
|
|
229
|
-
readFile(join(root, 'scripts', '_temporary_bh.py'), 'utf8'),
|
|
230
|
-
]);
|
|
231
208
|
await writeFile(configFile, JSON.stringify({
|
|
232
209
|
prompt: request.prompt,
|
|
233
210
|
file: request.files ?? [],
|
|
234
|
-
close_policy:
|
|
235
|
-
release_file: '',
|
|
211
|
+
close_policy: 'never',
|
|
236
212
|
}));
|
|
237
213
|
const startOutput = await browserWorkspaceCommand(cli, ['session', 'start', '--workspace', CHATGPT_BROWSER_WORKSPACE]);
|
|
238
214
|
const started = parseWorkspaceJson(startOutput);
|
|
@@ -242,12 +218,7 @@ function sessionBrowserWorkerRunner(root) {
|
|
|
242
218
|
activeSessionId = started.session_id;
|
|
243
219
|
if (signal.aborted)
|
|
244
220
|
throw new Error('browser-worker execution aborted');
|
|
245
|
-
const
|
|
246
|
-
const source = [
|
|
247
|
-
readiness,
|
|
248
|
-
submitSource.replace(/^from browser_harness import \*\s*$/m, ''),
|
|
249
|
-
temporarySource.replace(/^_wait_for_release\(\)\s*$/m, ''),
|
|
250
|
-
].join('\n\n').replaceAll('__CFG_PATH__', escapedConfig);
|
|
221
|
+
const source = await browserWorkspaceCommand(cli, ['platform', 'source', 'chatgpt', 'temporary-submit', '--config', configFile]);
|
|
251
222
|
const execOutput = await browserWorkspaceCommand(cli, ['session', 'exec', activeSessionId], source);
|
|
252
223
|
const wrapper = parseWorkspaceJson(execOutput);
|
|
253
224
|
if (wrapper.ok !== true) {
|
|
@@ -316,11 +287,9 @@ export class ChatGptAdapter {
|
|
|
316
287
|
name = 'chatgpt';
|
|
317
288
|
id = 'chatgpt/browser-worker';
|
|
318
289
|
capabilities = ['model', 'chatgpt', 'browser-workspace'];
|
|
319
|
-
agentPath;
|
|
320
290
|
runner;
|
|
321
291
|
tabClosePolicy;
|
|
322
292
|
constructor(options = {}) {
|
|
323
|
-
this.agentPath = options.agentPath ?? process.env.AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT;
|
|
324
293
|
this.runner = options.agentRunner;
|
|
325
294
|
this.tabClosePolicy = chatGptTabClosePolicy(options.tabClosePolicy ?? process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY);
|
|
326
295
|
}
|
|
@@ -344,11 +313,11 @@ export class ChatGptAdapter {
|
|
|
344
313
|
execution.launched = new Promise((resolve, reject) => { launchResolve = resolve; launchReject = reject; });
|
|
345
314
|
execution.launched.catch(() => { });
|
|
346
315
|
execution.promise = (async () => {
|
|
347
|
-
const
|
|
316
|
+
const definition = await readFile(new URL('../agents/chatgpt-worker.agent.md', import.meta.url), 'utf8');
|
|
348
317
|
if (controller.signal.aborted)
|
|
349
318
|
throw new Error('browser-worker execution aborted');
|
|
350
319
|
const policy = chatGptTabClosePolicy(task.chatgptTabClosePolicy, this.tabClosePolicy);
|
|
351
|
-
const run = (this.runner ?? sessionBrowserWorkerRunner(
|
|
320
|
+
const run = (this.runner ?? sessionBrowserWorkerRunner())(definition, { prompt: task.input, ...(task.attachments?.length ? { files: task.attachments } : {}), tabClosePolicy: policy }, route, controller.signal);
|
|
352
321
|
try {
|
|
353
322
|
const output = await (run.submitted ?? run.promise);
|
|
354
323
|
const response = parseBrowserWorkerResponse(output);
|
package/dist/cli-blocker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GitHubStore } from './store.js';
|
|
2
2
|
import { githubAuthContext } from './github-auth.js';
|
|
3
3
|
import { registerBlockerDependency } from './blockers.js';
|
|
4
|
-
import { arg,
|
|
4
|
+
import { arg, publishDependentUnblocked, repository, withEventBus } from './cli-support.js';
|
|
5
5
|
import { usage } from './cli-help.js';
|
|
6
6
|
export async function runBlockerCommand(action, args) {
|
|
7
7
|
if (!['add', 'resolve', 'show'].includes(action))
|
|
@@ -19,7 +19,7 @@ export async function runBlockerCommand(action, args) {
|
|
|
19
19
|
}
|
|
20
20
|
if (action === 'resolve') {
|
|
21
21
|
const summary = await blockerStore.resolveBlockerDependents(id, 'verified');
|
|
22
|
-
await
|
|
22
|
+
await withEventBus(args, bus => publishDependentUnblocked(bus, summary.unblocked, `Blocker Job ${id} resolved (verified)`));
|
|
23
23
|
console.log(JSON.stringify({ blocker: id, resolution: 'verified', ...summary }, null, 2));
|
|
24
24
|
return;
|
|
25
25
|
}
|
package/dist/cli-support.js
CHANGED
|
@@ -139,6 +139,15 @@ export async function eventBus(args) {
|
|
|
139
139
|
process.stderr.write(`${eventBusResolutionMessage(resolution)}\n`);
|
|
140
140
|
return resolution.bus;
|
|
141
141
|
}
|
|
142
|
+
export async function withEventBus(args, work, resolve = eventBus) {
|
|
143
|
+
const bus = await resolve(args);
|
|
144
|
+
try {
|
|
145
|
+
return await work(bus);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await bus?.close?.();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
142
151
|
export async function publishWake(bus, job, taskId, message) { if (!bus)
|
|
143
152
|
return; try {
|
|
144
153
|
await bus.publish(eventFor(job.id, taskId, null, 'job.wake', 'queued', message, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
|
package/dist/cli-unblock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { arg,
|
|
1
|
+
import { arg, publishTaskUnblocked, publishWake, saveLocal, withEventBus } from './cli-support.js';
|
|
2
2
|
import { isEffectivelyBlocked, unblockAutonomousTasks, unblockTask } from './unblock.js';
|
|
3
3
|
export async function runTaskUnblockCommand(args, store, job, task, localFile) {
|
|
4
4
|
const reason = arg(args, '--reason');
|
|
@@ -7,9 +7,10 @@ export async function runTaskUnblockCommand(args, store, job, task, localFile) {
|
|
|
7
7
|
const result = await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
|
|
8
8
|
const refreshed = await store.load(job.id);
|
|
9
9
|
await saveLocal(localFile, refreshed);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
await withEventBus(args, async (bus) => {
|
|
11
|
+
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
|
12
|
+
await publishWake(bus, refreshed, task.id, `Task ${task.id} unblocked${result.replanning ? ' for replanning' : ''}`);
|
|
13
|
+
});
|
|
13
14
|
console.log(JSON.stringify({ taskId: task.id, ...result, task: refreshed.tasks.find(item => item.id === task.id) }, null, 2));
|
|
14
15
|
}
|
|
15
16
|
export async function runJobUnblockCommand(args, store, job) {
|
|
@@ -22,19 +23,21 @@ export async function runJobUnblockCommand(args, store, job) {
|
|
|
22
23
|
if (job.executionMode === 'autonomous') {
|
|
23
24
|
const planTrigger = await unblockAutonomousTasks({ store, job, tasks: blocked, trigger: { source: 'cli', reason } });
|
|
24
25
|
const refreshed = await store.load(job.id);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
await withEventBus(args, async (bus) => {
|
|
27
|
+
for (const task of blocked)
|
|
28
|
+
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
|
29
|
+
await publishWake(bus, refreshed, 'job', `Job ${job.id} unblocked for replanning`);
|
|
30
|
+
});
|
|
29
31
|
console.log(JSON.stringify({ jobId: job.id, replanning: true, planTrigger }, null, 2));
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
for (const task of blocked)
|
|
33
35
|
await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
|
|
34
36
|
const refreshed = await store.load(job.id);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
await withEventBus(args, async (bus) => {
|
|
38
|
+
for (const task of blocked)
|
|
39
|
+
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
|
40
|
+
await publishWake(bus, refreshed, 'job', `Job ${job.id} unblocked`);
|
|
41
|
+
});
|
|
39
42
|
console.log(JSON.stringify({ jobId: job.id, replanning: false, resumedTasks: blocked.map(task => task.id) }, null, 2));
|
|
40
43
|
}
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ import { parseAgentGraph } from './agent-graph.js';
|
|
|
29
29
|
import { runJobUnblockCommand, runTaskUnblockCommand } from './cli-unblock.js';
|
|
30
30
|
import { createJobEntity, createTaskEntity } from './workflow/entity-creation.js';
|
|
31
31
|
export { helpText } from './cli-help.js';
|
|
32
|
-
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, } from './cli-support.js';
|
|
32
|
+
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';
|
|
33
33
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
34
34
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
35
35
|
if (!options.id)
|
|
@@ -185,7 +185,7 @@ async function runTaskCommand(action, args) {
|
|
|
185
185
|
}
|
|
186
186
|
const durableTask = (await store.load(job.id)).tasks.find(item => item.id === task.id) ?? task;
|
|
187
187
|
await saveLocal(localFile, await store.load(job.id));
|
|
188
|
-
await
|
|
188
|
+
await withEventBus(args, bus => publishWake(bus, refreshed, task.id, `Task ${task.id} created`));
|
|
189
189
|
console.log(JSON.stringify(durableTask, null, 2));
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
@@ -228,7 +228,7 @@ async function runTaskCommand(action, args) {
|
|
|
228
228
|
outputTask = outputJob.tasks.find(item => item.id === id) ?? result.task;
|
|
229
229
|
}
|
|
230
230
|
await saveLocal(localFile, outputJob);
|
|
231
|
-
await
|
|
231
|
+
await withEventBus(args, bus => publishWake(bus, outputJob, outputTask.id, `${outputTask.id} ${action} requested`));
|
|
232
232
|
console.log(JSON.stringify(outputTask, null, 2));
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
@@ -397,7 +397,7 @@ async function runJobCommand(action, args) {
|
|
|
397
397
|
await transitionJobEvent(job, 'pr.merged');
|
|
398
398
|
await store.saveJob(job);
|
|
399
399
|
const resolved = await store.resolveBlockerDependents(id, 'merge');
|
|
400
|
-
await
|
|
400
|
+
await withEventBus(args, bus => publishDependentUnblocked(bus, resolved.unblocked, `Blocker Job ${id} resolved (merge)`));
|
|
401
401
|
console.log(JSON.stringify(await store.load(id), null, 2));
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
@@ -597,7 +597,6 @@ async function main() {
|
|
|
597
597
|
console.log(`Initialized ${job.id}`);
|
|
598
598
|
return;
|
|
599
599
|
}
|
|
600
|
-
const bus = await eventBus(args);
|
|
601
600
|
if (command === 'status') {
|
|
602
601
|
const host = arg(args, '--host', '127.0.0.1');
|
|
603
602
|
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|
|
@@ -619,15 +618,12 @@ async function main() {
|
|
|
619
618
|
const { store, job, localFile } = loaded;
|
|
620
619
|
if (command === 'plan') {
|
|
621
620
|
const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
|
|
622
|
-
|
|
621
|
+
await withEventBus(args, async (bus) => {
|
|
623
622
|
const result = await runtime(store, args, bus).planJob(job.id, guidance?.trim() || undefined);
|
|
624
623
|
await saveLocal(localFile, result.job);
|
|
625
624
|
await publishWake(bus, result.job, result.round.id, `Planning round ${result.round.id} completed`);
|
|
626
625
|
console.log(JSON.stringify(result.round, null, 2));
|
|
627
|
-
}
|
|
628
|
-
finally {
|
|
629
|
-
await bus?.close?.();
|
|
630
|
-
}
|
|
626
|
+
});
|
|
631
627
|
return;
|
|
632
628
|
}
|
|
633
629
|
if (command === 'agent-register') {
|
|
@@ -659,27 +655,24 @@ async function main() {
|
|
|
659
655
|
console.log(`Recorded ${id}`);
|
|
660
656
|
return;
|
|
661
657
|
}
|
|
662
|
-
if (command === '
|
|
663
|
-
|
|
664
|
-
await requestManagedReconciliation(bus, job);
|
|
665
|
-
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
666
|
-
}
|
|
667
|
-
finally {
|
|
668
|
-
await bus?.close?.();
|
|
669
|
-
}
|
|
658
|
+
if (command === 'serve') {
|
|
659
|
+
await startService(args);
|
|
670
660
|
return;
|
|
671
661
|
}
|
|
672
|
-
const reconciler = runtime(store, args, bus);
|
|
673
662
|
if (command === 'reconcile') {
|
|
674
|
-
await
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
663
|
+
await withEventBus(args, async (bus) => {
|
|
664
|
+
if (!localFile) {
|
|
665
|
+
await requestManagedReconciliation(bus, job);
|
|
666
|
+
console.log(JSON.stringify(await store.load(job.id), null, 2));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const reconciler = runtime(store, args, bus);
|
|
670
|
+
await reconciler.reconcile(job.id);
|
|
671
|
+
await reconciler.idle();
|
|
672
|
+
const finalState = await store.load(job.id);
|
|
673
|
+
await saveLocal(localFile, finalState);
|
|
674
|
+
console.log(JSON.stringify(finalState, null, 2));
|
|
675
|
+
});
|
|
683
676
|
return;
|
|
684
677
|
}
|
|
685
678
|
throw new Error(usage);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-relay",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.44",
|
|
4
4
|
"description": "Durable async agent jobs coordinated through GitHub pull requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/relayd.js dist/relayd-main.js",
|
|
12
12
|
"test": "npm run build && node --test test/*.test.js",
|
|
13
|
-
"test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1
|
|
13
|
+
"test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1 sh -c \"npm run build && node test/quality/run-temporary-chat.mjs\"",
|
|
14
14
|
"dev": "tsx src/cli.ts",
|
|
15
15
|
"test:publish": "npm run build && node --test --test-force-exit test/*.test.js"
|
|
16
16
|
},
|
|
@@ -91,7 +91,7 @@ Leaf workers consume the resolved instruction/skill/agent paths, not the full to
|
|
|
91
91
|
|
|
92
92
|
## ChatGPT worker
|
|
93
93
|
|
|
94
|
-
The ChatGPT adapter uses
|
|
94
|
+
The ChatGPT adapter uses Relay's `chatgpt-worker` agent contract and Browser Workspace ChatGPT platform; it does not depend on a Neo worker skill. 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.
|
|
95
95
|
|
|
96
96
|
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.
|
|
97
97
|
|