doer-agent 0.5.8 → 0.6.0

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/agent.js CHANGED
@@ -2,22 +2,18 @@ import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { StringCodec } from "nats";
5
- import { buildAgentSettingsEnvPatch, readAgentModelInstructions, readAgentSettingsConfig, resolveAgentModelInstructionsFilePath, } from "./agent-settings.js";
5
+ import { buildAgentSettingsEnvPatch, readAgentSettingsConfig, } from "./agent-settings.js";
6
6
  import { handleFsRpcMessage } from "./agent-fs-rpc.js";
7
7
  import { handleGitRpcMessage } from "./agent-git-rpc.js";
8
- import { subscribeToCodexAuthRpc } from "./agent-codex-auth-rpc.js";
8
+ import { subscribeToCodexAppRpc } from "./agent-codex-app-rpc.js";
9
+ import { createCodexAppServerManager } from "./codex-app-server-manager.js";
9
10
  import { subscribeToDaemonRpc } from "./agent-daemon-rpc.js";
10
- import { buildDaemonMcpConfigArgs, buildDatabaseMcpConfigArgs, buildManagedCodexArgs, createLocalCodexCliTools, normalizeCodexModel, normalizeShellRpcCodexAuthBundle, spawnManagedCodexCommand, } from "./agent-codex-cli.js";
11
+ import { createLocalCodexCliTools } from "./agent-codex-cli.js";
11
12
  import { connectBootstrapWithRetry } from "./agent-jetstream.js";
12
- import { prepareCommandExecution } from "./agent-run-execution.js";
13
- import { attachManagedRunProcessLifecycle, createPendingRunSessionTracker } from "./agent-run-lifecycle.js";
14
- import { claimRunStartSlot, cloneRunTask, getStoredRun, listPersistedRunTasks, persistRunTask, publishImmediateRunEvent, pruneStaleRunsDir, releaseRunStartSlot, removeRunTask, updateRunStartSlotSession, } from "./agent-run-state.js";
15
13
  import { runConnectedAgentSession } from "./agent-session-loop.js";
16
14
  import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
17
- import { prepareCodexAuthBundle, sendSignalToPid, sendSignalToTaskProcess, } from "./agent-task-execution.js";
18
- import { collectSessionJsonlFiles, detectPendingRunSession, findSessionFilePathBySessionId, stopAllSessionWatchers, subscribeToSessionRpc, } from "./agent-session-rpc.js";
19
- import { handleNonStartRunRpc, normalizeRunRpcRequest, publishRunRpcResponse, } from "./agent-run-rpc.js";
20
- import { buildAgentCodexAuthRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentRunEventsSubject, buildAgentRunRpcSubject, buildAgentSessionRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, normalizeEnvPatch, filterValidRunImagePaths, normalizeRunImagePaths, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, writeRunStatus, writeRunStream, } from "./agent-runtime-utils.js";
15
+ import { sendSignalToTaskProcess } from "./agent-task-execution.js";
16
+ import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
21
17
  import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
22
18
  import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
23
19
  import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
@@ -27,12 +23,12 @@ const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
27
23
  const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
28
24
  const HEARTBEAT_INTERVAL_MS = 5_000;
29
25
  const HEARTBEAT_FAILURE_THRESHOLD = 3;
26
+ const codexAppEventCodec = StringCodec();
30
27
  let activeTaskLogContext = null;
31
28
  let workspaceRootOverride = null;
32
29
  function resolveWorkspaceRoot() {
33
30
  return workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
34
31
  }
35
- const runRpcCodec = StringCodec();
36
32
  function writeAgentInfo(message) {
37
33
  process.stdout.write(`[doer-agent] ${message}\n`);
38
34
  eventPersistenceHelpers.emitAgentMetaLog("info", message);
@@ -49,143 +45,6 @@ function writeAgentInfraError(message) {
49
45
  // Keep heartbeat/connectivity failures non-fatal.
50
46
  }
51
47
  }
52
- async function updateRunSessionMetadata(task, metadata) {
53
- let changed = false;
54
- const previousSessionId = task.sessionId;
55
- if (!task.sessionId && typeof metadata.sessionId === "string" && metadata.sessionId.trim()) {
56
- task.sessionId = metadata.sessionId.trim();
57
- changed = true;
58
- }
59
- if (!task.sessionFilePath && typeof metadata.sessionFilePath === "string" && metadata.sessionFilePath.trim()) {
60
- task.sessionFilePath = metadata.sessionFilePath.trim();
61
- changed = true;
62
- }
63
- if (!task.sessionFilePath && task.sessionId) {
64
- const resolvedSessionFilePath = await findSessionFilePathBySessionId(resolveWorkspaceRoot(), task.sessionId).catch(() => null);
65
- if (resolvedSessionFilePath) {
66
- task.sessionFilePath = resolvedSessionFilePath;
67
- changed = true;
68
- }
69
- }
70
- if (!changed) {
71
- return;
72
- }
73
- task.updatedAt = formatLocalTimestamp();
74
- await persistRunTask(resolveWorkspaceRoot(), task).catch(() => undefined);
75
- if (metadata.nc) {
76
- publishImmediateRunEvent({
77
- nc: metadata.nc,
78
- userId: task.userId,
79
- task,
80
- buildRunEventsSubject: buildAgentRunEventsSubject,
81
- });
82
- }
83
- if (!previousSessionId && task.sessionId) {
84
- await updateRunStartSlotSession({
85
- workspaceRoot: resolveWorkspaceRoot(),
86
- runId: task.id,
87
- previousSessionId,
88
- sessionId: task.sessionId,
89
- formatTimestamp: formatLocalTimestamp,
90
- }).catch(() => undefined);
91
- }
92
- }
93
- async function startManagedRun(args) {
94
- const prepared = await prepareCommandExecution({
95
- cwd: args.cwd,
96
- userId: args.userId,
97
- taskId: args.runId,
98
- codexAuthBundle: args.codexAuthBundle,
99
- runtimeEnvPatch: args.runtimeEnvPatch,
100
- agentProjectDir: AGENT_PROJECT_DIR,
101
- resolveShellPath: runtimeEnvHelpers.resolveShellPath,
102
- resolveTaskWorkspace: runtimeEnvHelpers.resolveTaskWorkspace,
103
- resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
104
- prepareCodexAuthBundle,
105
- readAgentSettingsConfig,
106
- resolveWorkspaceRoot,
107
- buildAgentSettingsEnvPatch,
108
- prepareTaskGitEnv: runtimeEnvHelpers.prepareTaskGitEnv,
109
- });
110
- const child = spawnManagedCodexCommand({
111
- codexArgs: args.codexArgs,
112
- taskWorkspace: prepared.taskWorkspace,
113
- env: prepared.env,
114
- agentToken: args.agentToken,
115
- });
116
- const now = formatLocalTimestamp();
117
- const task = {
118
- id: args.runId,
119
- userId: args.userId,
120
- agentId: args.agentId,
121
- processPid: typeof child.pid === "number" ? child.pid : null,
122
- sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
123
- sessionFilePath: null,
124
- status: "running",
125
- cancelRequested: false,
126
- resultExitCode: null,
127
- resultSignal: null,
128
- error: null,
129
- createdAt: now,
130
- updatedAt: now,
131
- startedAt: now,
132
- finishedAt: null,
133
- };
134
- let pendingSessionPollClosed = false;
135
- const knownPendingSessionFiles = new Set();
136
- const pendingSessionTracker = createPendingRunSessionTracker({
137
- task,
138
- detectPendingRunSession: async () => detectPendingRunSession(resolveWorkspaceRoot(), knownPendingSessionFiles),
139
- updateRunSessionMetadata: async (metadata) => updateRunSessionMetadata(task, { ...metadata, nc: args.nc }),
140
- });
141
- const stopPendingSessionPoll = () => {
142
- pendingSessionPollClosed = true;
143
- pendingSessionTracker.stop();
144
- };
145
- const pollPendingSession = async () => {
146
- if (pendingSessionPollClosed) {
147
- stopPendingSessionPoll();
148
- return;
149
- }
150
- await pendingSessionTracker.poll();
151
- };
152
- if (!task.sessionId) {
153
- const existingFiles = await collectSessionJsonlFiles(resolveWorkspaceRoot()).catch(() => []);
154
- for (const file of existingFiles) {
155
- knownPendingSessionFiles.add(file.filePath);
156
- }
157
- pendingSessionTracker.start();
158
- }
159
- child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
160
- child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
161
- attachManagedRunProcessLifecycle({
162
- child,
163
- task,
164
- nc: args.nc,
165
- stopPendingSessionPoll,
166
- getStoredRun: (runId) => getStoredRun(resolveWorkspaceRoot(), runId),
167
- publishImmediateRunEvent: (eventArgs) => publishImmediateRunEvent({
168
- ...eventArgs,
169
- buildRunEventsSubject: buildAgentRunEventsSubject,
170
- }),
171
- removeRunTask: (runId) => removeRunTask(resolveWorkspaceRoot(), runId),
172
- releaseRunStartSlot: ({ runId, sessionId }) => releaseRunStartSlot({
173
- workspaceRoot: resolveWorkspaceRoot(),
174
- runId,
175
- sessionId,
176
- }),
177
- codexAuthCleanup: prepared.codexAuthCleanup,
178
- writeRunStatus,
179
- formatTimestamp: formatLocalTimestamp,
180
- });
181
- void persistRunTask(resolveWorkspaceRoot(), task).catch(() => undefined);
182
- publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task, buildRunEventsSubject: buildAgentRunEventsSubject });
183
- writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
184
- if (!task.sessionId) {
185
- void pollPendingSession();
186
- }
187
- return cloneRunTask(task);
188
- }
189
48
  function subscribeToSettingsRpc(args) {
190
49
  const subject = buildAgentSettingsRpcSubject(args.userId, args.agentId);
191
50
  args.jetstream.nc.subscribe(subject, {
@@ -200,6 +59,7 @@ function subscribeToSettingsRpc(args) {
200
59
  nc: args.jetstream.nc,
201
60
  agentId: args.agentId,
202
61
  workspaceRoot: resolveWorkspaceRoot(),
62
+ onSettingsUpdated: () => args.codexAppServerManager.restart("settings updated"),
203
63
  onError: writeAgentError,
204
64
  });
205
65
  },
@@ -225,140 +85,6 @@ function subscribeToGitRpc(args) {
225
85
  });
226
86
  writeAgentInfo(`git rpc subscribed subject=${subject}`);
227
87
  }
228
- async function handleRunRpcMessage(args) {
229
- let requestId = "unknown";
230
- let responseSubject = "";
231
- try {
232
- const payload = JSON.parse(runRpcCodec.decode(args.msg.data));
233
- const request = normalizeRunRpcRequest({
234
- request: payload,
235
- agentId: args.agentId,
236
- normalizeModel: normalizeCodexModel,
237
- normalizeImagePaths: normalizeRunImagePaths,
238
- normalizeEnvPatch,
239
- normalizeCodexAuthBundle: normalizeShellRpcCodexAuthBundle,
240
- });
241
- requestId = request.requestId;
242
- responseSubject = request.responseSubject;
243
- if (request.action === "start") {
244
- const runId = request.runId ?? requestId;
245
- await claimRunStartSlot({
246
- workspaceRoot: resolveWorkspaceRoot(),
247
- runId,
248
- sessionId: request.sessionId,
249
- formatTimestamp: formatLocalTimestamp,
250
- });
251
- try {
252
- const workspaceRoot = resolveWorkspaceRoot();
253
- const localAgentSettings = await readAgentSettingsConfig({ workspaceRoot });
254
- const customInstructions = await readAgentModelInstructions(workspaceRoot);
255
- const validImagePaths = await filterValidRunImagePaths({
256
- workspaceRoot,
257
- imagePaths: request.imagePaths,
258
- onInvalidImage: (imagePath, reason) => {
259
- writeRunStatus(runId, `skipping invalid image path=${imagePath} reason=${reason}`);
260
- },
261
- });
262
- const task = await startManagedRun({
263
- requestId,
264
- runId,
265
- serverBaseUrl: args.serverBaseUrl,
266
- userId: args.userId,
267
- agentId: args.agentId,
268
- nc: args.jetstream.nc,
269
- sessionId: request.sessionId,
270
- codexArgs: buildManagedCodexArgs({
271
- prompt: request.prompt ?? "",
272
- imagePaths: validImagePaths,
273
- sessionId: request.sessionId,
274
- model: request.model,
275
- personality: localAgentSettings.general.personality,
276
- modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath(workspaceRoot) : null,
277
- configOverrides: [
278
- ...buildDaemonMcpConfigArgs({
279
- agentProjectDir: AGENT_PROJECT_DIR,
280
- workspaceRoot,
281
- }),
282
- ...buildDatabaseMcpConfigArgs({
283
- agentProjectDir: AGENT_PROJECT_DIR,
284
- workspaceRoot,
285
- }),
286
- "--config",
287
- `features.computer_use=${localAgentSettings.codex.computerUseEnabled ? "true" : "false"}`,
288
- "--config",
289
- `features.browser_use=${localAgentSettings.codex.browserUseEnabled ? "true" : "false"}`,
290
- ],
291
- }),
292
- cwd: request.cwd,
293
- runtimeEnvPatch: request.runtimeEnvPatch,
294
- codexAuthBundle: request.codexAuthBundle,
295
- agentToken: args.agentToken,
296
- });
297
- publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
298
- }
299
- catch (error) {
300
- await releaseRunStartSlot({
301
- workspaceRoot: resolveWorkspaceRoot(),
302
- runId,
303
- sessionId: request.sessionId,
304
- }).catch(() => undefined);
305
- throw error;
306
- }
307
- return;
308
- }
309
- await handleNonStartRunRpc({
310
- request,
311
- nc: args.jetstream.nc,
312
- userId: args.userId,
313
- agentId: args.agentId,
314
- listPersistedRunTasks: async () => listPersistedRunTasks(resolveWorkspaceRoot()),
315
- cloneRunTask,
316
- getStoredRun: async (runId) => getStoredRun(resolveWorkspaceRoot(), runId),
317
- persistRunTask: async (task) => persistRunTask(resolveWorkspaceRoot(), task),
318
- publishImmediateRunEvent: (task) => publishImmediateRunEvent({
319
- nc: args.jetstream.nc,
320
- userId: task.userId,
321
- task,
322
- buildRunEventsSubject: buildAgentRunEventsSubject,
323
- }),
324
- writeRunStatus,
325
- sendSignalToPid,
326
- formatTimestamp: formatLocalTimestamp,
327
- });
328
- }
329
- catch (error) {
330
- const message = error instanceof Error ? error.message : String(error);
331
- if (responseSubject) {
332
- publishRunRpcResponse({
333
- nc: args.jetstream.nc,
334
- responseSubject,
335
- payload: { requestId, ok: false, error: message },
336
- });
337
- }
338
- writeAgentError(`run rpc failed requestId=${requestId} error=${message}`);
339
- }
340
- }
341
- function subscribeToRunRpc(args) {
342
- const subject = buildAgentRunRpcSubject(args.userId, args.agentId);
343
- args.jetstream.nc.subscribe(subject, {
344
- callback: (error, msg) => {
345
- if (error) {
346
- const message = error instanceof Error ? error.message : String(error);
347
- writeAgentError(`run rpc subscription error: ${message}`);
348
- return;
349
- }
350
- void handleRunRpcMessage({
351
- msg,
352
- jetstream: args.jetstream,
353
- serverBaseUrl: args.serverBaseUrl,
354
- userId: args.userId,
355
- agentId: args.agentId,
356
- agentToken: args.agentToken,
357
- });
358
- },
359
- });
360
- writeAgentInfo(`run rpc subscribed subject=${subject}`);
361
- }
362
88
  function subscribeToFsRpc(args) {
363
89
  const subject = buildAgentFsRpcSubject(args.userId, args.agentId);
364
90
  args.jetstream.nc.subscribe(subject, {
@@ -380,6 +106,20 @@ function subscribeToFsRpc(args) {
380
106
  });
381
107
  writeAgentInfo(`fs rpc subscribed subject=${subject}`);
382
108
  }
109
+ function formatCodexAppNotificationParams(params) {
110
+ if (params === undefined) {
111
+ return "undefined";
112
+ }
113
+ try {
114
+ return JSON.stringify(params);
115
+ }
116
+ catch {
117
+ return String(params);
118
+ }
119
+ }
120
+ function shouldForwardCodexAppNotification(method) {
121
+ return method !== "item/agentMessage/delta";
122
+ }
383
123
  const runtimeEnvHelpers = createRuntimeEnvHelpers({
384
124
  resolveWorkspaceRoot,
385
125
  agentProjectDir: AGENT_PROJECT_DIR,
@@ -421,8 +161,6 @@ async function main() {
421
161
  process.env.WORKSPACE = startupWorkspaceRoot;
422
162
  process.env.CODEX_HOME = path.join(startupWorkspaceRoot, ".codex");
423
163
  await mkdir(process.env.CODEX_HOME, { recursive: true }).catch(() => undefined);
424
- // Preserve run state for processes that are still alive after an agent restart.
425
- await pruneStaleRunsDir(resolveWorkspaceRoot());
426
164
  const serverBaseUrlRaw = resolveArgOrEnv(args, ["server", "url"], ["DOER_AGENT_SERVER"], DEFAULT_SERVER_BASE_URL);
427
165
  const requestedServerBaseUrl = serverBaseUrlRaw.replace(/\/$/, "");
428
166
  const serverBaseUrl = resolveContainerReachableServerBaseUrl(requestedServerBaseUrl);
@@ -481,6 +219,26 @@ async function main() {
481
219
  formatTimestamp: formatLocalTimestamp,
482
220
  heartbeatAgentSession: heartbeatSession,
483
221
  subscribeAll: () => {
222
+ const codexAppServerManager = createCodexAppServerManager({
223
+ workspaceRoot: resolveWorkspaceRoot(),
224
+ resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
225
+ readAgentSettingsConfig,
226
+ onLog: writeAgentInfo,
227
+ onNotification: (method, params) => {
228
+ if (!shouldForwardCodexAppNotification(method)) {
229
+ return;
230
+ }
231
+ writeAgentInfo(`codex app-server notification method=${method} params=${formatCodexAppNotificationParams(params)}`);
232
+ const event = {
233
+ type: "codex.app.notification",
234
+ agentId: initialAgentId,
235
+ method,
236
+ params,
237
+ emittedAt: new Date().toISOString(),
238
+ };
239
+ jetstream.nc.publish(buildAgentCodexAppEventsSubject(userId, initialAgentId), codexAppEventCodec.encode(JSON.stringify(event)));
240
+ },
241
+ });
484
242
  subscribeToFsRpc({
485
243
  jetstream,
486
244
  serverBaseUrl,
@@ -499,27 +257,12 @@ async function main() {
499
257
  onInfo: writeAgentInfo,
500
258
  onError: writeAgentError,
501
259
  });
502
- subscribeToSessionRpc({
503
- nc: jetstream.nc,
504
- subject: buildAgentSessionRpcSubject(userId, initialAgentId),
505
- agentId: initialAgentId,
506
- workspaceRoot: resolveWorkspaceRoot(),
507
- onInfo: writeAgentInfo,
508
- onError: writeAgentError,
509
- formatTimestamp: formatLocalTimestamp,
510
- });
511
- subscribeToCodexAuthRpc({
260
+ subscribeToCodexAppRpc({
512
261
  nc: jetstream.nc,
513
- subject: buildAgentCodexAuthRpcSubject(userId, initialAgentId),
262
+ subject: buildAgentCodexAppRpcSubject(userId, initialAgentId),
263
+ eventsSubject: buildAgentCodexAppEventsSubject(userId, initialAgentId),
514
264
  agentId: initialAgentId,
515
- workspaceRoot: resolveWorkspaceRoot(),
516
- buildLocalCodexCliCommand: localCodexCliTools.buildLocalCodexCliCommand,
517
- resolveShellPath: runtimeEnvHelpers.resolveShellPath,
518
- resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
519
- runLocalCodexCli: localCodexCliTools.runLocalCodexCli,
520
- runLocalCodexCliWithInput: localCodexCliTools.runLocalCodexCliWithInput,
521
- sendSignalToTaskProcess,
522
- stripAnsi: localCodexCliTools.stripAnsi,
265
+ manager: codexAppServerManager,
523
266
  onInfo: writeAgentInfo,
524
267
  onError: writeAgentError,
525
268
  });
@@ -527,6 +270,7 @@ async function main() {
527
270
  jetstream,
528
271
  userId,
529
272
  agentId: initialAgentId,
273
+ codexAppServerManager,
530
274
  });
531
275
  subscribeToGitRpc({
532
276
  jetstream,
@@ -546,15 +290,7 @@ async function main() {
546
290
  onInfo: writeAgentInfo,
547
291
  onError: writeAgentError,
548
292
  });
549
- subscribeToRunRpc({
550
- jetstream,
551
- serverBaseUrl,
552
- userId,
553
- agentId: initialAgentId,
554
- agentToken,
555
- });
556
293
  },
557
- stopAllSessionWatchers: () => stopAllSessionWatchers({ onError: writeAgentError }),
558
294
  onInfraError: writeAgentInfraError,
559
295
  sleep,
560
296
  });
@@ -0,0 +1,148 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ export class CodexAppServerClient {
4
+ options;
5
+ child = null;
6
+ stdoutLines = null;
7
+ nextRequestId = 1;
8
+ startPromise = null;
9
+ pending = new Map();
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ async request(method, params) {
14
+ await this.start();
15
+ return await this.requestStarted(method, params);
16
+ }
17
+ async notify(method, params) {
18
+ await this.start();
19
+ const child = this.child;
20
+ if (!child || child.killed) {
21
+ throw new Error("Codex app-server is not running");
22
+ }
23
+ const payload = params === undefined ? { method } : { method, params };
24
+ child.stdin.write(`${JSON.stringify(payload)}\n`, "utf8");
25
+ }
26
+ async stop() {
27
+ const child = this.child;
28
+ if (!child || child.killed) {
29
+ return;
30
+ }
31
+ child.kill("SIGTERM");
32
+ }
33
+ async start() {
34
+ if (this.child && !this.child.killed) {
35
+ return;
36
+ }
37
+ if (this.startPromise) {
38
+ return await this.startPromise;
39
+ }
40
+ this.startPromise = this.startInner();
41
+ try {
42
+ await this.startPromise;
43
+ }
44
+ finally {
45
+ this.startPromise = null;
46
+ }
47
+ }
48
+ async startInner() {
49
+ this.child = spawn("codex", this.options.args, {
50
+ cwd: this.options.cwd,
51
+ env: this.options.env,
52
+ stdio: ["pipe", "pipe", "pipe"],
53
+ });
54
+ this.child.stdout.setEncoding("utf8");
55
+ this.child.stderr.setEncoding("utf8");
56
+ this.stdoutLines = createInterface({ input: this.child.stdout });
57
+ this.stdoutLines.on("line", (line) => this.handleLine(line));
58
+ this.child.stderr.on("data", (chunk) => {
59
+ const message = chunk.trim();
60
+ if (message) {
61
+ this.options.onLog?.(`[codex-app-server] ${message}`);
62
+ }
63
+ });
64
+ this.child.once("exit", (code, signal) => {
65
+ this.options.onLog?.(`[codex-app-server] exited code=${code ?? "null"} signal=${signal ?? "null"}`);
66
+ this.rejectPending(new Error("Codex app-server exited"));
67
+ this.stdoutLines?.close();
68
+ this.stdoutLines = null;
69
+ this.child = null;
70
+ });
71
+ await this.requestStarted("initialize", {
72
+ clientInfo: {
73
+ name: "doer-agent",
74
+ title: "Doer Agent",
75
+ version: "0.5.9",
76
+ },
77
+ capabilities: {
78
+ experimentalApi: true,
79
+ },
80
+ });
81
+ await this.notify("initialized");
82
+ }
83
+ async requestStarted(method, params) {
84
+ const child = this.child;
85
+ if (!child || child.killed) {
86
+ throw new Error("Codex app-server is not running");
87
+ }
88
+ const id = this.nextRequestId++;
89
+ const payload = params === undefined ? { id, method } : { id, method, params };
90
+ const timeoutMs = this.options.requestTimeoutMs ?? 30_000;
91
+ return await new Promise((resolve, reject) => {
92
+ const timer = setTimeout(() => {
93
+ this.pending.delete(id);
94
+ reject(new Error(`Timed out waiting for Codex app-server response: ${method}`));
95
+ }, timeoutMs);
96
+ this.pending.set(id, { resolve, reject, timer });
97
+ child.stdin.write(`${JSON.stringify(payload)}\n`, "utf8", (error) => {
98
+ if (!error) {
99
+ return;
100
+ }
101
+ clearTimeout(timer);
102
+ this.pending.delete(id);
103
+ reject(error);
104
+ });
105
+ });
106
+ }
107
+ handleLine(line) {
108
+ let message;
109
+ try {
110
+ message = JSON.parse(line);
111
+ }
112
+ catch {
113
+ this.options.onLog?.("[codex-app-server] ignored malformed JSON line");
114
+ return;
115
+ }
116
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
117
+ return;
118
+ }
119
+ const record = message;
120
+ if (record.id !== undefined) {
121
+ const id = Number(record.id);
122
+ const pending = Number.isInteger(id) ? this.pending.get(id) : null;
123
+ if (!pending) {
124
+ return;
125
+ }
126
+ this.pending.delete(id);
127
+ clearTimeout(pending.timer);
128
+ if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
129
+ const error = record.error;
130
+ pending.reject(new Error(typeof error.message === "string" ? error.message : "Codex app-server request failed"));
131
+ }
132
+ else {
133
+ pending.resolve(record.result);
134
+ }
135
+ return;
136
+ }
137
+ if (typeof record.method === "string") {
138
+ this.options.onNotification?.(record.method, record.params);
139
+ }
140
+ }
141
+ rejectPending(error) {
142
+ for (const [id, pending] of this.pending) {
143
+ this.pending.delete(id);
144
+ clearTimeout(pending.timer);
145
+ pending.reject(error);
146
+ }
147
+ }
148
+ }