doer-agent 0.9.10 → 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.
@@ -190,20 +190,27 @@ async function handleCodexAppRpcMessage(args) {
190
190
  }
191
191
  export function subscribeToCodexAppRpc(args) {
192
192
  const handoffJobs = new CodexThreadHandoffJobManager(async (input, onProgress) => {
193
- try {
194
- const result = await createCodexThreadHandoff({
195
- manager: args.manager,
196
- ...input,
197
- onProgress,
198
- onLog: args.onInfo,
199
- });
200
- args.onInfo(`thread handoff completed sourceThreadId=${input.sourceThreadId} targetThreadId=${result.threadId}`);
201
- return result;
202
- }
203
- catch (error) {
204
- args.onError(`thread handoff failed sourceThreadId=${input.sourceThreadId} error=${error instanceof Error ? error.message : String(error)}`);
205
- throw error;
206
- }
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
+ },
207
214
  });
208
215
  args.nc.closed().finally(() => {
209
216
  void args.manager.stop().catch(() => undefined);
@@ -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).catch(() => undefined);
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
  }
@@ -8,14 +8,16 @@ function copySnapshot(job) {
8
8
  }
9
9
  export class CodexThreadHandoffJobManager {
10
10
  run;
11
- terminalTtlMs;
12
- maxJobs;
13
11
  jobs = new Map();
14
12
  activeJobBySourceThreadId = new Map();
15
- constructor(run, terminalTtlMs = 30 * 60_000, maxJobs = 100) {
13
+ terminalTtlMs;
14
+ maxJobs;
15
+ onStatus;
16
+ constructor(run, options = {}) {
16
17
  this.run = run;
17
- this.terminalTtlMs = terminalTtlMs;
18
- this.maxJobs = maxJobs;
18
+ this.terminalTtlMs = options.terminalTtlMs ?? 30 * 60_000;
19
+ this.maxJobs = options.maxJobs ?? 100;
20
+ this.onStatus = options.onStatus;
19
21
  }
20
22
  start(input) {
21
23
  this.cleanup();
@@ -37,6 +39,7 @@ export class CodexThreadHandoffJobManager {
37
39
  };
38
40
  this.jobs.set(job.jobId, job);
39
41
  this.activeJobBySourceThreadId.set(input.sourceThreadId, job.jobId);
42
+ this.emitStatus(job);
40
43
  setImmediate(() => {
41
44
  void this.execute(job.jobId, input);
42
45
  });
@@ -55,6 +58,7 @@ export class CodexThreadHandoffJobManager {
55
58
  }
56
59
  current.phase = phase;
57
60
  current.updatedAt = new Date().toISOString();
61
+ this.emitStatus(current);
58
62
  };
59
63
  try {
60
64
  const result = await this.run(input, updatePhase);
@@ -63,6 +67,7 @@ export class CodexThreadHandoffJobManager {
63
67
  current.phase = "completed";
64
68
  current.result = result;
65
69
  current.updatedAt = new Date().toISOString();
70
+ this.emitStatus(current);
66
71
  }
67
72
  }
68
73
  catch (error) {
@@ -71,6 +76,7 @@ export class CodexThreadHandoffJobManager {
71
76
  current.phase = "failed";
72
77
  current.error = error instanceof Error ? error.message : String(error);
73
78
  current.updatedAt = new Date().toISOString();
79
+ this.emitStatus(current);
74
80
  }
75
81
  }
76
82
  finally {
@@ -79,6 +85,9 @@ export class CodexThreadHandoffJobManager {
79
85
  }
80
86
  }
81
87
  }
88
+ emitStatus(job) {
89
+ this.onStatus?.(copySnapshot(job));
90
+ }
82
91
  cleanup() {
83
92
  const cutoff = Date.now() - this.terminalTtlMs;
84
93
  for (const [jobId, job] of this.jobs) {
@@ -17,11 +17,14 @@ async function nextTick() {
17
17
  }
18
18
  test("starts handoff work asynchronously and reports progress", async () => {
19
19
  let finish;
20
+ const statuses = [];
20
21
  const manager = new CodexThreadHandoffJobManager(async (input, onProgress) => {
21
22
  onProgress("collecting");
22
23
  return await new Promise((resolve) => {
23
24
  finish = resolve;
24
25
  });
26
+ }, {
27
+ onStatus: (job) => statuses.push(job.phase),
25
28
  });
26
29
  const started = manager.start({ sourceThreadId: "source-thread" });
27
30
  assert.equal(started.job.phase, "queued");
@@ -33,6 +36,7 @@ test("starts handoff work asynchronously and reports progress", async () => {
33
36
  const completed = manager.get(started.job.jobId);
34
37
  assert.equal(completed?.phase, "completed");
35
38
  assert.equal(completed?.result?.threadId, "target-thread");
39
+ assert.deepEqual(statuses, ["queued", "collecting", "completed"]);
36
40
  });
37
41
  test("deduplicates active jobs for the same source thread", () => {
38
42
  const manager = new CodexThreadHandoffJobManager(async () => {
@@ -44,8 +48,11 @@ test("deduplicates active jobs for the same source thread", () => {
44
48
  assert.equal(second.job.jobId, first.job.jobId);
45
49
  });
46
50
  test("keeps a failed job status for polling", async () => {
51
+ const statuses = [];
47
52
  const manager = new CodexThreadHandoffJobManager(async () => {
48
53
  throw new Error("summary failed");
54
+ }, {
55
+ onStatus: (job) => statuses.push({ phase: job.phase, error: job.error }),
49
56
  });
50
57
  const started = manager.start({ sourceThreadId: "source-thread" });
51
58
  await nextTick();
@@ -53,4 +60,8 @@ test("keeps a failed job status for polling", async () => {
53
60
  const failed = manager.get(started.job.jobId);
54
61
  assert.equal(failed?.phase, "failed");
55
62
  assert.equal(failed?.error, "summary failed");
63
+ assert.deepEqual(statuses, [
64
+ { phase: "queued", error: undefined },
65
+ { phase: "failed", error: "summary failed" },
66
+ ]);
56
67
  });
@@ -319,6 +319,7 @@ export async function createCodexThreadHandoff(args) {
319
319
  turnsCollected: collected.turnCount,
320
320
  turnsOmitted,
321
321
  }),
322
+ onLog: args.onLog,
322
323
  });
323
324
  const handoff = boundedText(handoffRaw, MAX_HANDOFF_CHARS);
324
325
  if (!handoff) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",