doer-agent 0.9.9 → 0.9.11
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
2
|
import { createCodexThreadHandoff, } from "./codex-thread-handoff.js";
|
|
3
|
+
import { CodexThreadHandoffJobManager } from "./codex-thread-handoff-jobs.js";
|
|
3
4
|
const codexAppRpcCodec = StringCodec();
|
|
4
5
|
const codexAppRpcOmitRules = [
|
|
5
6
|
{
|
|
@@ -84,7 +85,7 @@ function normalizeCodexAppRpcRequest(args) {
|
|
|
84
85
|
}
|
|
85
86
|
return { requestId, action: "mcp-oauth-logout", name };
|
|
86
87
|
}
|
|
87
|
-
if (actionRaw === "thread-handoff") {
|
|
88
|
+
if (actionRaw === "thread-handoff" || actionRaw === "thread-handoff-start") {
|
|
88
89
|
const sourceThreadId = typeof args.request.sourceThreadId === "string" ? args.request.sourceThreadId.trim() : "";
|
|
89
90
|
const targetName = typeof args.request.targetName === "string" ? args.request.targetName.trim() : "";
|
|
90
91
|
const activeTurnId = typeof args.request.activeTurnId === "string" ? args.request.activeTurnId.trim() : "";
|
|
@@ -102,7 +103,7 @@ function normalizeCodexAppRpcRequest(args) {
|
|
|
102
103
|
}
|
|
103
104
|
return {
|
|
104
105
|
requestId,
|
|
105
|
-
action:
|
|
106
|
+
action: actionRaw,
|
|
106
107
|
sourceThreadId,
|
|
107
108
|
targetName,
|
|
108
109
|
activeTurnId,
|
|
@@ -110,6 +111,13 @@ function normalizeCodexAppRpcRequest(args) {
|
|
|
110
111
|
sourceGoal,
|
|
111
112
|
};
|
|
112
113
|
}
|
|
114
|
+
if (actionRaw === "thread-handoff-status") {
|
|
115
|
+
const jobId = typeof args.request.jobId === "string" ? args.request.jobId.trim() : "";
|
|
116
|
+
if (!jobId) {
|
|
117
|
+
throw new Error("invalid thread handoff status request");
|
|
118
|
+
}
|
|
119
|
+
return { requestId, action: "thread-handoff-status", jobId };
|
|
120
|
+
}
|
|
113
121
|
if (actionRaw !== "request" || !method) {
|
|
114
122
|
throw new Error("invalid codex app rpc request");
|
|
115
123
|
}
|
|
@@ -127,21 +135,43 @@ async function handleCodexAppRpcMessage(args) {
|
|
|
127
135
|
const payload = JSON.parse(codexAppRpcCodec.decode(args.msg.data));
|
|
128
136
|
const request = normalizeCodexAppRpcRequest({ request: payload, agentId: args.agentId });
|
|
129
137
|
requestId = request.requestId;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
let result;
|
|
139
|
+
if (request.action === "request") {
|
|
140
|
+
result = applyCodexAppRpcOmitRules(request.method, await args.manager.request(request.method, request.params, request.timeoutMs));
|
|
141
|
+
}
|
|
142
|
+
else if (request.action === "mcp-oauth-callback") {
|
|
143
|
+
result = await args.manager.relayMcpOauthCallback(request.path, request.search);
|
|
144
|
+
}
|
|
145
|
+
else if (request.action === "mcp-oauth-logout") {
|
|
146
|
+
result = await args.manager.logoutMcpServer(request.name);
|
|
147
|
+
}
|
|
148
|
+
else if (request.action === "thread-handoff-status") {
|
|
149
|
+
const job = args.handoffJobs.get(request.jobId);
|
|
150
|
+
if (!job) {
|
|
151
|
+
throw new Error("thread handoff job not found");
|
|
152
|
+
}
|
|
153
|
+
result = job;
|
|
154
|
+
}
|
|
155
|
+
else if (request.action === "thread-handoff-start") {
|
|
156
|
+
result = args.handoffJobs.start({
|
|
157
|
+
sourceThreadId: request.sourceThreadId,
|
|
158
|
+
targetName: request.targetName,
|
|
159
|
+
activeTurnId: request.activeTurnId,
|
|
160
|
+
latestUserText: request.latestUserText,
|
|
161
|
+
sourceGoal: request.sourceGoal,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
result = await createCodexThreadHandoff({
|
|
166
|
+
manager: args.manager,
|
|
167
|
+
sourceThreadId: request.sourceThreadId,
|
|
168
|
+
targetName: request.targetName,
|
|
169
|
+
activeTurnId: request.activeTurnId,
|
|
170
|
+
latestUserText: request.latestUserText,
|
|
171
|
+
sourceGoal: request.sourceGoal,
|
|
172
|
+
onLog: args.onInfo,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
145
175
|
args.msg.respond(codexAppRpcCodec.encode(JSON.stringify({
|
|
146
176
|
requestId,
|
|
147
177
|
ok: true,
|
|
@@ -159,6 +189,29 @@ async function handleCodexAppRpcMessage(args) {
|
|
|
159
189
|
}
|
|
160
190
|
}
|
|
161
191
|
export function subscribeToCodexAppRpc(args) {
|
|
192
|
+
const handoffJobs = new CodexThreadHandoffJobManager(async (input, onProgress) => {
|
|
193
|
+
return await createCodexThreadHandoff({
|
|
194
|
+
manager: args.manager,
|
|
195
|
+
...input,
|
|
196
|
+
onProgress,
|
|
197
|
+
onLog: args.onInfo,
|
|
198
|
+
});
|
|
199
|
+
}, {
|
|
200
|
+
onStatus: (job) => {
|
|
201
|
+
const elapsedMs = Math.max(0, Date.parse(job.updatedAt) - Date.parse(job.createdAt));
|
|
202
|
+
const target = job.result?.threadId ? ` targetThreadId=${job.result.threadId}` : "";
|
|
203
|
+
const error = job.error
|
|
204
|
+
? ` error=${JSON.stringify(job.error.replace(/\s+/g, " ").slice(0, 500))}`
|
|
205
|
+
: "";
|
|
206
|
+
const message = `thread handoff job status jobId=${job.jobId} sourceThreadId=${job.sourceThreadId} phase=${job.phase} elapsedMs=${elapsedMs}${target}${error}`;
|
|
207
|
+
if (job.phase === "failed") {
|
|
208
|
+
args.onError(message);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
args.onInfo(message);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
162
215
|
args.nc.closed().finally(() => {
|
|
163
216
|
void args.manager.stop().catch(() => undefined);
|
|
164
217
|
});
|
|
@@ -174,6 +227,7 @@ export function subscribeToCodexAppRpc(args) {
|
|
|
174
227
|
nc: args.nc,
|
|
175
228
|
agentId: args.agentId,
|
|
176
229
|
manager: args.manager,
|
|
230
|
+
handoffJobs,
|
|
177
231
|
onInfo: args.onInfo,
|
|
178
232
|
onError: args.onError,
|
|
179
233
|
});
|
package/dist/codex-text-task.js
CHANGED
|
@@ -37,14 +37,17 @@ function buildCodexUserInput(prompt) {
|
|
|
37
37
|
}];
|
|
38
38
|
}
|
|
39
39
|
export async function runCodexTextTask(args) {
|
|
40
|
+
const startedAt = Date.now();
|
|
40
41
|
let threadId = "";
|
|
41
42
|
let turnId = "";
|
|
42
43
|
let output = "";
|
|
44
|
+
let outputStarted = false;
|
|
43
45
|
let settled = false;
|
|
44
46
|
let cleanupNotification = () => { };
|
|
45
47
|
let settle = (_callback) => { };
|
|
46
48
|
const completed = new Promise((resolve, reject) => {
|
|
47
49
|
const timeout = setTimeout(() => {
|
|
50
|
+
args.onLog?.(`codex text task status phase=timed_out threadId=${threadId || "pending"} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
48
51
|
settle(() => reject(new Error("Timed out while waiting for Codex text task")));
|
|
49
52
|
}, args.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
50
53
|
settle = (callback) => {
|
|
@@ -69,11 +72,16 @@ export async function runCodexTextTask(args) {
|
|
|
69
72
|
if (method === "item/agentMessage/delta") {
|
|
70
73
|
const record = recordValue(params);
|
|
71
74
|
output += stringValue(record?.delta) || stringValue(record?.text);
|
|
75
|
+
if (!outputStarted && output) {
|
|
76
|
+
outputStarted = true;
|
|
77
|
+
args.onLog?.(`codex text task status phase=responding threadId=${threadId} turnId=${turnId || eventTurnId || "pending"} elapsedMs=${Date.now() - startedAt}`);
|
|
78
|
+
}
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
81
|
if (!isTerminalTurnMethod(method)) {
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
84
|
+
args.onLog?.(`codex text task status phase=terminal_notification method=${method} threadId=${threadId} turnId=${turnId || eventTurnId || "pending"} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
77
85
|
if (method === "turn/completed") {
|
|
78
86
|
settle(resolve);
|
|
79
87
|
return;
|
|
@@ -92,6 +100,7 @@ export async function runCodexTextTask(args) {
|
|
|
92
100
|
if (!threadId) {
|
|
93
101
|
throw new Error("Codex app-server did not return a text task thread id");
|
|
94
102
|
}
|
|
103
|
+
args.onLog?.(`codex text task status phase=thread_started threadId=${threadId} elapsedMs=${Date.now() - startedAt}`);
|
|
95
104
|
const turnResult = recordValue(await args.manager.request("turn/start", {
|
|
96
105
|
threadId,
|
|
97
106
|
input: buildCodexUserInput(args.prompt),
|
|
@@ -100,16 +109,25 @@ export async function runCodexTextTask(args) {
|
|
|
100
109
|
if (!turnId) {
|
|
101
110
|
throw new Error("Codex app-server did not return a text task turn id");
|
|
102
111
|
}
|
|
112
|
+
args.onLog?.(`codex text task status phase=turn_started threadId=${threadId} turnId=${turnId} elapsedMs=${Date.now() - startedAt}`);
|
|
103
113
|
await completed.finally(() => cleanupNotification());
|
|
114
|
+
args.onLog?.(`codex text task status phase=completed threadId=${threadId} turnId=${turnId} elapsedMs=${Date.now() - startedAt} outputChars=${output.length}`);
|
|
104
115
|
return output;
|
|
105
116
|
}
|
|
106
117
|
catch (error) {
|
|
107
118
|
settle(() => { });
|
|
119
|
+
args.onLog?.(`codex text task status phase=failed threadId=${threadId || "pending"} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} error=${JSON.stringify((error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 500))}`);
|
|
108
120
|
throw error;
|
|
109
121
|
}
|
|
110
122
|
finally {
|
|
111
123
|
if (threadId) {
|
|
112
|
-
await args.manager.request("thread/unsubscribe", { threadId }, 30_000)
|
|
124
|
+
await args.manager.request("thread/unsubscribe", { threadId }, 30_000)
|
|
125
|
+
.then(() => {
|
|
126
|
+
args.onLog?.(`codex text task status phase=unsubscribed threadId=${threadId} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt}`);
|
|
127
|
+
})
|
|
128
|
+
.catch((error) => {
|
|
129
|
+
args.onLog?.(`codex text task status phase=unsubscribe_failed threadId=${threadId} turnId=${turnId || "pending"} elapsedMs=${Date.now() - startedAt} error=${JSON.stringify((error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 500))}`);
|
|
130
|
+
});
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const TERMINAL_PHASES = new Set(["completed", "failed"]);
|
|
3
|
+
function copySnapshot(job) {
|
|
4
|
+
return {
|
|
5
|
+
...job,
|
|
6
|
+
...(job.result ? { result: { ...job.result } } : {}),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export class CodexThreadHandoffJobManager {
|
|
10
|
+
run;
|
|
11
|
+
jobs = new Map();
|
|
12
|
+
activeJobBySourceThreadId = new Map();
|
|
13
|
+
terminalTtlMs;
|
|
14
|
+
maxJobs;
|
|
15
|
+
onStatus;
|
|
16
|
+
constructor(run, options = {}) {
|
|
17
|
+
this.run = run;
|
|
18
|
+
this.terminalTtlMs = options.terminalTtlMs ?? 30 * 60_000;
|
|
19
|
+
this.maxJobs = options.maxJobs ?? 100;
|
|
20
|
+
this.onStatus = options.onStatus;
|
|
21
|
+
}
|
|
22
|
+
start(input) {
|
|
23
|
+
this.cleanup();
|
|
24
|
+
const existingJobId = this.activeJobBySourceThreadId.get(input.sourceThreadId);
|
|
25
|
+
const existing = existingJobId ? this.jobs.get(existingJobId) : undefined;
|
|
26
|
+
if (existing && !TERMINAL_PHASES.has(existing.phase)) {
|
|
27
|
+
return { job: copySnapshot(existing), deduplicated: true };
|
|
28
|
+
}
|
|
29
|
+
if (this.jobs.size >= this.maxJobs) {
|
|
30
|
+
throw new Error("too many thread handoff jobs");
|
|
31
|
+
}
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
const job = {
|
|
34
|
+
jobId: randomUUID(),
|
|
35
|
+
sourceThreadId: input.sourceThreadId,
|
|
36
|
+
phase: "queued",
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
};
|
|
40
|
+
this.jobs.set(job.jobId, job);
|
|
41
|
+
this.activeJobBySourceThreadId.set(input.sourceThreadId, job.jobId);
|
|
42
|
+
this.emitStatus(job);
|
|
43
|
+
setImmediate(() => {
|
|
44
|
+
void this.execute(job.jobId, input);
|
|
45
|
+
});
|
|
46
|
+
return { job: copySnapshot(job), deduplicated: false };
|
|
47
|
+
}
|
|
48
|
+
get(jobId) {
|
|
49
|
+
this.cleanup();
|
|
50
|
+
const job = this.jobs.get(jobId);
|
|
51
|
+
return job ? copySnapshot(job) : null;
|
|
52
|
+
}
|
|
53
|
+
async execute(jobId, input) {
|
|
54
|
+
const updatePhase = (phase) => {
|
|
55
|
+
const current = this.jobs.get(jobId);
|
|
56
|
+
if (!current || TERMINAL_PHASES.has(current.phase)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
current.phase = phase;
|
|
60
|
+
current.updatedAt = new Date().toISOString();
|
|
61
|
+
this.emitStatus(current);
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.run(input, updatePhase);
|
|
65
|
+
const current = this.jobs.get(jobId);
|
|
66
|
+
if (current) {
|
|
67
|
+
current.phase = "completed";
|
|
68
|
+
current.result = result;
|
|
69
|
+
current.updatedAt = new Date().toISOString();
|
|
70
|
+
this.emitStatus(current);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const current = this.jobs.get(jobId);
|
|
75
|
+
if (current) {
|
|
76
|
+
current.phase = "failed";
|
|
77
|
+
current.error = error instanceof Error ? error.message : String(error);
|
|
78
|
+
current.updatedAt = new Date().toISOString();
|
|
79
|
+
this.emitStatus(current);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (this.activeJobBySourceThreadId.get(input.sourceThreadId) === jobId) {
|
|
84
|
+
this.activeJobBySourceThreadId.delete(input.sourceThreadId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
emitStatus(job) {
|
|
89
|
+
this.onStatus?.(copySnapshot(job));
|
|
90
|
+
}
|
|
91
|
+
cleanup() {
|
|
92
|
+
const cutoff = Date.now() - this.terminalTtlMs;
|
|
93
|
+
for (const [jobId, job] of this.jobs) {
|
|
94
|
+
if (TERMINAL_PHASES.has(job.phase) && Date.parse(job.updatedAt) < cutoff) {
|
|
95
|
+
this.jobs.delete(jobId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (this.jobs.size < this.maxJobs) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const terminalJobs = [...this.jobs.values()]
|
|
102
|
+
.filter((job) => TERMINAL_PHASES.has(job.phase))
|
|
103
|
+
.sort((left, right) => Date.parse(left.updatedAt) - Date.parse(right.updatedAt));
|
|
104
|
+
while (this.jobs.size >= this.maxJobs && terminalJobs.length > 0) {
|
|
105
|
+
const oldest = terminalJobs.shift();
|
|
106
|
+
if (oldest) {
|
|
107
|
+
this.jobs.delete(oldest.jobId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { CodexThreadHandoffJobManager } from "./codex-thread-handoff-jobs.js";
|
|
4
|
+
function handoffResult(sourceThreadId) {
|
|
5
|
+
return {
|
|
6
|
+
sourceThreadId,
|
|
7
|
+
thread: { id: "target-thread" },
|
|
8
|
+
threadId: "target-thread",
|
|
9
|
+
handoff: "summary",
|
|
10
|
+
turnsCollected: 2,
|
|
11
|
+
turnsOmitted: 0,
|
|
12
|
+
warnings: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function nextTick() {
|
|
16
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
17
|
+
}
|
|
18
|
+
test("starts handoff work asynchronously and reports progress", async () => {
|
|
19
|
+
let finish;
|
|
20
|
+
const statuses = [];
|
|
21
|
+
const manager = new CodexThreadHandoffJobManager(async (input, onProgress) => {
|
|
22
|
+
onProgress("collecting");
|
|
23
|
+
return await new Promise((resolve) => {
|
|
24
|
+
finish = resolve;
|
|
25
|
+
});
|
|
26
|
+
}, {
|
|
27
|
+
onStatus: (job) => statuses.push(job.phase),
|
|
28
|
+
});
|
|
29
|
+
const started = manager.start({ sourceThreadId: "source-thread" });
|
|
30
|
+
assert.equal(started.job.phase, "queued");
|
|
31
|
+
assert.equal(started.deduplicated, false);
|
|
32
|
+
await nextTick();
|
|
33
|
+
assert.equal(manager.get(started.job.jobId)?.phase, "collecting");
|
|
34
|
+
finish?.(handoffResult("source-thread"));
|
|
35
|
+
await nextTick();
|
|
36
|
+
const completed = manager.get(started.job.jobId);
|
|
37
|
+
assert.equal(completed?.phase, "completed");
|
|
38
|
+
assert.equal(completed?.result?.threadId, "target-thread");
|
|
39
|
+
assert.deepEqual(statuses, ["queued", "collecting", "completed"]);
|
|
40
|
+
});
|
|
41
|
+
test("deduplicates active jobs for the same source thread", () => {
|
|
42
|
+
const manager = new CodexThreadHandoffJobManager(async () => {
|
|
43
|
+
return await new Promise(() => undefined);
|
|
44
|
+
});
|
|
45
|
+
const first = manager.start({ sourceThreadId: "source-thread" });
|
|
46
|
+
const second = manager.start({ sourceThreadId: "source-thread" });
|
|
47
|
+
assert.equal(second.deduplicated, true);
|
|
48
|
+
assert.equal(second.job.jobId, first.job.jobId);
|
|
49
|
+
});
|
|
50
|
+
test("keeps a failed job status for polling", async () => {
|
|
51
|
+
const statuses = [];
|
|
52
|
+
const manager = new CodexThreadHandoffJobManager(async () => {
|
|
53
|
+
throw new Error("summary failed");
|
|
54
|
+
}, {
|
|
55
|
+
onStatus: (job) => statuses.push({ phase: job.phase, error: job.error }),
|
|
56
|
+
});
|
|
57
|
+
const started = manager.start({ sourceThreadId: "source-thread" });
|
|
58
|
+
await nextTick();
|
|
59
|
+
await nextTick();
|
|
60
|
+
const failed = manager.get(started.job.jobId);
|
|
61
|
+
assert.equal(failed?.phase, "failed");
|
|
62
|
+
assert.equal(failed?.error, "summary failed");
|
|
63
|
+
assert.deepEqual(statuses, [
|
|
64
|
+
{ phase: "queued", error: undefined },
|
|
65
|
+
{ phase: "failed", error: "summary failed" },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
@@ -265,6 +265,7 @@ export async function createCodexThreadHandoff(args) {
|
|
|
265
265
|
}
|
|
266
266
|
const warnings = [];
|
|
267
267
|
const sourceGoal = normalizeGoal(args.sourceGoal);
|
|
268
|
+
args.onProgress?.("preparing");
|
|
268
269
|
if (sourceGoal?.status === "active") {
|
|
269
270
|
await args.manager.request("thread/goal/set", {
|
|
270
271
|
threadId: sourceThreadId,
|
|
@@ -282,6 +283,7 @@ export async function createCodexThreadHandoff(args) {
|
|
|
282
283
|
warnings.push(`Could not interrupt source turn: ${error instanceof Error ? error.message : String(error)}`);
|
|
283
284
|
});
|
|
284
285
|
}
|
|
286
|
+
args.onProgress?.("collecting");
|
|
285
287
|
args.onLog?.(`collecting handoff history sourceThreadId=${sourceThreadId}`);
|
|
286
288
|
const sourceRead = recordValue(await args.manager.request("thread/read", {
|
|
287
289
|
threadId: sourceThreadId,
|
|
@@ -304,6 +306,7 @@ export async function createCodexThreadHandoff(args) {
|
|
|
304
306
|
}
|
|
305
307
|
const turnsOmitted = Math.max(0, collected.turnCount - bounded.includedTurns)
|
|
306
308
|
+ (collected.truncated ? 1 : 0);
|
|
309
|
+
args.onProgress?.("summarizing");
|
|
307
310
|
args.onLog?.(`summarizing handoff sourceThreadId=${sourceThreadId} turns=${collected.turnCount}`);
|
|
308
311
|
const handoffRaw = await runCodexTextTask({
|
|
309
312
|
manager: args.manager,
|
|
@@ -316,11 +319,13 @@ export async function createCodexThreadHandoff(args) {
|
|
|
316
319
|
turnsCollected: collected.turnCount,
|
|
317
320
|
turnsOmitted,
|
|
318
321
|
}),
|
|
322
|
+
onLog: args.onLog,
|
|
319
323
|
});
|
|
320
324
|
const handoff = boundedText(handoffRaw, MAX_HANDOFF_CHARS);
|
|
321
325
|
if (!handoff) {
|
|
322
326
|
throw new Error("Codex did not return a thread handoff summary");
|
|
323
327
|
}
|
|
328
|
+
args.onProgress?.("creating");
|
|
324
329
|
args.onLog?.(`creating handoff target sourceThreadId=${sourceThreadId}`);
|
|
325
330
|
const startResult = recordValue(await args.manager.request("thread/start", {
|
|
326
331
|
cwd: sourceCwd || null,
|
|
@@ -333,6 +338,7 @@ export async function createCodexThreadHandoff(args) {
|
|
|
333
338
|
throw new Error("Codex app-server did not return a handoff target thread");
|
|
334
339
|
}
|
|
335
340
|
try {
|
|
341
|
+
args.onProgress?.("injecting");
|
|
336
342
|
await args.manager.request("thread/inject_items", {
|
|
337
343
|
threadId,
|
|
338
344
|
items: [{
|
|
@@ -349,6 +355,7 @@ export async function createCodexThreadHandoff(args) {
|
|
|
349
355
|
await args.manager.request("thread/delete", { threadId }, 30_000).catch(() => undefined);
|
|
350
356
|
throw error;
|
|
351
357
|
}
|
|
358
|
+
args.onProgress?.("finalizing");
|
|
352
359
|
const targetName = stringValue(args.targetName).slice(0, 200);
|
|
353
360
|
if (targetName) {
|
|
354
361
|
await args.manager.request("thread/name/set", {
|