@parall/codex-agent 1.28.1 → 1.29.1

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.
@@ -7,20 +7,6 @@ type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, "approvalPolicy" | "c
7
7
  contextFilePath?: string;
8
8
  useParallProvider?: boolean;
9
9
  };
10
- /**
11
- * Bridge driver backed by `codex app-server --listen stdio://`.
12
- *
13
- * Lifecycle:
14
- * - bridge startup: spawn one `codex app-server` subprocess, do the
15
- * `initialize` / `initialized` handshake once, keep the pipe open
16
- * - per Parall dispatch: `thread/start` (first time) or `thread/resume`,
17
- * then `turn/start`; forward server notifications until `turn/completed`
18
- * - fork-on-busy: `thread/fork` creates a disposable sibling thread
19
- *
20
- * Concurrency: agent-core routes one dispatch per sessionKey at a time, so
21
- * main + fork can interleave turns on the same stdio pipe. We route
22
- * notifications by threadId the server stamps on every item/turn event.
23
- */
24
10
  export declare class CodexAppServerAdapter implements DispatchAdapter {
25
11
  private readonly opts;
26
12
  private client;
@@ -28,6 +14,8 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
28
14
  private initialized;
29
15
  private startPromise;
30
16
  private readonly activeTurns;
17
+ private readonly activeTurnIds;
18
+ private readonly pendingInjections;
31
19
  private readonly resumedThreadIds;
32
20
  private stopping;
33
21
  /**
@@ -44,6 +32,8 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
44
32
  model?: string | null;
45
33
  reasoningEffort?: string | null;
46
34
  }): void;
35
+ enqueueDuringDispatch(sessionKey: string, body: string): Promise<boolean>;
36
+ hasPendingInjections(sessionKey: string): boolean;
47
37
  dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
48
38
  getBranchPoint(_sessionKey: string): string | undefined;
49
39
  forkSession({ sessionKey: parentSessionKey }: ForkOpts): Promise<ForkSessionHandle | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAK/E,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAwMxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAyBb,OAAO;IA8DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IA2CxB,OAAO,CAAC,iBAAiB;CA2B1B"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAmCF,qBAAa,qBAAsB,YAAW,eAAe;IA+B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA9BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAKhF,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB/E,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA6NxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA+BhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAiBI,aAAa;YA2Bb,OAAO;IAyErB,OAAO,CAAC,qBAAqB;YAmCf,UAAU;IA2CxB,OAAO,CAAC,iBAAiB;CA4B1B"}
package/dist/dispatch.js CHANGED
@@ -1,5 +1,4 @@
1
- import { spawn } from "node:child_process";
2
- import { execSync } from "node:child_process";
1
+ import { execSync, spawn } from "node:child_process";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import * as fs from "node:fs";
5
4
  import { appendPreparedLocalAttachmentRefs, ensureLocalAttachmentGitExclude, pinLocalAttachmentPaths, } from "@parall/agent-core/internal/attachment-input";
@@ -20,6 +19,21 @@ import { JsonRpcStdioClient } from "./jsonrpc-client.js";
20
19
  * main + fork can interleave turns on the same stdio pipe. We route
21
20
  * notifications by threadId the server stamps on every item/turn event.
22
21
  */
22
+ const IS_WIN32 = process.platform === "win32";
23
+ function quoteWin32Arg(arg) {
24
+ if (!/[\s"&|^<>()]/.test(arg))
25
+ return arg;
26
+ return `"${arg.replace(/"/g, '""')}"`;
27
+ }
28
+ function killWin32Tree(pid) {
29
+ try {
30
+ execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
23
37
  export class CodexAppServerAdapter {
24
38
  opts;
25
39
  client = null;
@@ -27,6 +41,8 @@ export class CodexAppServerAdapter {
27
41
  initialized = false;
28
42
  startPromise = null;
29
43
  activeTurns = new Map();
44
+ activeTurnIds = new Map();
45
+ pendingInjections = new Map();
30
46
  resumedThreadIds = new Set();
31
47
  stopping = false;
32
48
  /**
@@ -55,7 +71,49 @@ export class CodexAppServerAdapter {
55
71
  if (config.reasoningEffort !== undefined)
56
72
  this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
57
73
  }
74
+ async enqueueDuringDispatch(sessionKey, body) {
75
+ const client = this.client;
76
+ if (!client || client.isDisposed())
77
+ return false;
78
+ const threadId = this.opts.sessionManager.getThreadId(sessionKey);
79
+ if (!threadId)
80
+ return false;
81
+ const turnId = this.activeTurnIds.get(threadId);
82
+ if (!turnId)
83
+ return false;
84
+ try {
85
+ await client.sendRequest("turn/steer", {
86
+ threadId,
87
+ expectedTurnId: turnId,
88
+ input: buildTurnInput(body, []),
89
+ });
90
+ this.pendingInjections.set(sessionKey, (this.pendingInjections.get(sessionKey) ?? 0) + 1);
91
+ return true;
92
+ }
93
+ catch (err) {
94
+ this.opts.log?.warn?.(`codex-agent: turn/steer failed: ${errToString(err)}`);
95
+ return false;
96
+ }
97
+ }
98
+ hasPendingInjections(sessionKey) {
99
+ return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
100
+ }
58
101
  async *dispatch({ event, bodyForAgent, sessionKey, context }) {
102
+ const pending = this.pendingInjections.get(sessionKey) ?? 0;
103
+ if (pending > 0) {
104
+ this.pendingInjections.delete(sessionKey);
105
+ const threadId = this.opts.sessionManager.getThreadId(sessionKey);
106
+ if (threadId && this.client && !this.client.isDisposed()) {
107
+ (this.opts.log ?? context.log)?.info?.(`codex-agent[${context.accountId}]: ${pending} steer injection(s) already sent; skipping turn/start`);
108
+ yield {
109
+ type: "runtime_session",
110
+ runtimeSessionId: threadId,
111
+ runtimeLaneKey: sessionKey,
112
+ };
113
+ return;
114
+ }
115
+ (this.opts.log ?? context.log)?.warn?.(`codex-agent[${context.accountId}]: pending steer invalidated (subprocess died); falling through to normal dispatch`);
116
+ }
59
117
  await this.ensureStarted(context.log);
60
118
  // After ensureStarted resolves the subprocess could still die before we
61
119
  // capture the client (handleSubprocessClose nulls this.client). Yield a
@@ -210,7 +268,9 @@ export class CodexAppServerAdapter {
210
268
  this.resumedThreadIds.add(freshThreadId);
211
269
  threadId = freshThreadId;
212
270
  }
213
- void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
271
+ const turnId = extractTurnId(turnStartResult);
272
+ if (turnId)
273
+ this.activeTurnIds.set(threadId, turnId);
214
274
  yield {
215
275
  type: "runtime_session",
216
276
  runtimeSessionId: threadId,
@@ -250,8 +310,8 @@ export class CodexAppServerAdapter {
250
310
  finally {
251
311
  releasePreparedAttachments();
252
312
  this.activeTurns.delete(threadId);
313
+ this.activeTurnIds.delete(threadId);
253
314
  if (!sawTurnEnd) {
254
- // Defensive: if we bailed early, make sure we leave no dangling sink.
255
315
  sink.close();
256
316
  }
257
317
  }
@@ -270,10 +330,19 @@ export class CodexAppServerAdapter {
270
330
  return null;
271
331
  const handle = this.opts.sessionManager.createForkSessionKey();
272
332
  try {
273
- const result = await client.sendRequest("thread/fork", {
333
+ const forkParams = {
274
334
  threadId: parentThreadId,
275
335
  ephemeral: true,
276
- });
336
+ };
337
+ if (this.opts.useParallProvider) {
338
+ forkParams.modelProvider = "parall";
339
+ }
340
+ if (this.opts.model)
341
+ forkParams.model = this.opts.model;
342
+ if (this.opts.reasoningEffort) {
343
+ forkParams.config = { modelReasoningEffort: this.opts.reasoningEffort };
344
+ }
345
+ const result = await client.sendRequest("thread/fork", forkParams);
277
346
  const forkedThreadId = extractThreadId(result);
278
347
  if (!forkedThreadId) {
279
348
  this.opts.log?.warn?.("codex-agent: thread/fork returned no thread id");
@@ -300,10 +369,14 @@ export class CodexAppServerAdapter {
300
369
  this.proc = null;
301
370
  this.client = null;
302
371
  this.initialized = false;
372
+ this.activeTurnIds.clear();
373
+ this.pendingInjections.clear();
303
374
  if (client)
304
375
  client.dispose(new Error("adapter stopped"));
305
376
  if (proc && proc.exitCode === null && proc.signalCode === null) {
306
- proc.kill("SIGTERM");
377
+ if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
378
+ proc.kill("SIGTERM");
379
+ }
307
380
  }
308
381
  }
309
382
  async ensureStarted(log) {
@@ -313,6 +386,8 @@ export class CodexAppServerAdapter {
313
386
  activeSink.close();
314
387
  }
315
388
  this.activeTurns.clear();
389
+ this.activeTurnIds.clear();
390
+ this.pendingInjections.clear();
316
391
  this.client = null;
317
392
  this.proc = null;
318
393
  this.initialized = false;
@@ -355,10 +430,11 @@ export class CodexAppServerAdapter {
355
430
  }
356
431
  const args = ["app-server", "--listen", "stdio://"];
357
432
  (log ?? this.opts.log)?.info?.(`codex-agent: spawning ${this.opts.codexBin} ${args.join(" ")}`);
358
- const proc = spawn(this.opts.codexBin, args, {
433
+ const proc = spawn(IS_WIN32 ? quoteWin32Arg(this.opts.codexBin) : this.opts.codexBin, IS_WIN32 ? args.map(quoteWin32Arg) : args, {
359
434
  cwd: this.opts.workspaceDir,
360
435
  env,
361
436
  stdio: ["pipe", "pipe", "pipe"],
437
+ shell: IS_WIN32,
362
438
  });
363
439
  proc.stderr.setEncoding("utf8");
364
440
  proc.stderr.on("data", (chunk) => {
@@ -369,7 +445,11 @@ export class CodexAppServerAdapter {
369
445
  // start), we kill the half-spawned subprocess and clear local refs here
370
446
  // — leaving them on `this` would leak the orphan process and cause the
371
447
  // next ensureStarted() to spawn a second app-server on top of it.
372
- const client = new JsonRpcStdioClient(proc);
448
+ const client = new JsonRpcStdioClient(proc, undefined, (p) => {
449
+ if (!IS_WIN32 || !p.pid || !killWin32Tree(p.pid)) {
450
+ p.kill("SIGTERM");
451
+ }
452
+ });
373
453
  client.setNotificationHandler((method, params) => this.routeNotification(method, params));
374
454
  proc.once("close", (code, signal) => this.handleSubprocessClose(proc, code, signal, log));
375
455
  proc.once("error", (err) => this.handleSubprocessClose(proc, null, null, log, err));
@@ -382,8 +462,11 @@ export class CodexAppServerAdapter {
382
462
  }
383
463
  catch (err) {
384
464
  client.dispose(err instanceof Error ? err : new Error(String(err)));
385
- if (proc.exitCode === null && proc.signalCode === null)
386
- proc.kill("SIGTERM");
465
+ if (proc.exitCode === null && proc.signalCode === null) {
466
+ if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
467
+ proc.kill("SIGTERM");
468
+ }
469
+ }
387
470
  throw err;
388
471
  }
389
472
  this.proc = proc;
@@ -413,6 +496,8 @@ export class CodexAppServerAdapter {
413
496
  activeSink.close();
414
497
  }
415
498
  this.activeTurns.clear();
499
+ this.activeTurnIds.clear();
500
+ this.pendingInjections.clear();
416
501
  this.resumedThreadIds.clear();
417
502
  this.client = null;
418
503
  this.proc = null;
@@ -478,6 +563,7 @@ export class CodexAppServerAdapter {
478
563
  sink.push({ kind: "runtime", event });
479
564
  }
480
565
  if (method === "turn/completed") {
566
+ this.activeTurnIds.delete(threadId);
481
567
  sink.push({ kind: "turn_end", threadId });
482
568
  }
483
569
  }
@@ -568,6 +654,17 @@ function extractThreadId(result) {
568
654
  return thread.id;
569
655
  return undefined;
570
656
  }
657
+ function extractTurnId(result) {
658
+ if (!result || typeof result !== "object")
659
+ return undefined;
660
+ const r = result;
661
+ if (typeof r.turnId === "string")
662
+ return r.turnId;
663
+ const turn = r.turn;
664
+ if (turn && typeof turn.id === "string")
665
+ return turn.id;
666
+ return undefined;
667
+ }
571
668
  function extractThreadIdFromNotification(params) {
572
669
  if (!params || typeof params !== "object")
573
670
  return undefined;
package/dist/index.js CHANGED
@@ -1,18 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from "node:os";
3
- import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
3
+ import { ParallAgentGateway, createPlatformConfigManager, createLogger, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
4
4
  import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
5
5
  import { buildCodexRuntimeKey, contextFilePathForSession, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
6
  import { CodexAppServerAdapter } from "./dispatch.js";
7
7
  import { CodexSessionManager } from "./session-manager.js";
8
8
  import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
9
- function createLogger(prefix) {
10
- return {
11
- info: (msg) => console.log(`[${prefix}] ${msg}`),
12
- warn: (msg) => console.warn(`[${prefix}] ${msg}`),
13
- error: (msg) => console.error(`[${prefix}] ${msg}`),
14
- };
15
- }
9
+ const log = createLogger("codex-agent");
16
10
  async function getAgentMeWithLegacyFallback(client, orgId) {
17
11
  try {
18
12
  return await client.getAgentMe(orgId);
@@ -27,7 +21,6 @@ async function getAgentMeWithLegacyFallback(client, orgId) {
27
21
  }
28
22
  async function main() {
29
23
  const config = resolveCodexAgentConfig(process.env);
30
- const log = createLogger("codex-agent");
31
24
  const client = new ParallClient({
32
25
  baseUrl: config.apiUrl,
33
26
  token: config.apiKey,
@@ -136,6 +129,6 @@ async function main() {
136
129
  }
137
130
  }
138
131
  main().catch((err) => {
139
- console.error(`[codex-agent] fatal: ${String(err)}`);
132
+ log.error(`fatal: ${String(err)}`);
140
133
  process.exitCode = 1;
141
134
  });
@@ -33,12 +33,13 @@ export type NotificationHandler = (method: string, params: unknown) => void;
33
33
  export declare class JsonRpcStdioClient {
34
34
  private readonly proc;
35
35
  private readonly requestTimeoutMs;
36
+ private readonly killProcess?;
36
37
  private nextId;
37
38
  private readonly pending;
38
39
  private buffer;
39
40
  private onNotification;
40
41
  private disposed;
41
- constructor(proc: ChildProcessWithoutNullStreams, requestTimeoutMs?: number);
42
+ constructor(proc: ChildProcessWithoutNullStreams, requestTimeoutMs?: number, killProcess?: ((proc: ChildProcessWithoutNullStreams) => void) | undefined);
42
43
  setNotificationHandler(handler: NotificationHandler): void;
43
44
  sendRequest(method: string, params?: unknown): Promise<unknown>;
44
45
  sendNotification(method: string, params?: unknown): void;
@@ -1 +1 @@
1
- {"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAW5E;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAQ3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IARnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,IAAI,EAAE,8BAA8B,EACpC,gBAAgB,GAAE,MAAmC;IASxE,sBAAsB,CAAC,OAAO,EAAE,mBAAmB;IAInD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB/D,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAMjD,UAAU,IAAI,OAAO;IAIrB,OAAO,CAAC,GAAG,EAAE,KAAK;IAUlB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,UAAU;CAyBnB"}
1
+ {"version":3,"file":"jsonrpc-client.d.ts","sourceRoot":"","sources":["../src/jsonrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAW5E;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAQ3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAT/B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,IAAI,EAAE,8BAA8B,EACpC,gBAAgB,GAAE,MAAmC,EACrD,WAAW,CAAC,GAAE,CAAC,IAAI,EAAE,8BAA8B,KAAK,IAAI,aAAA;IAS/E,sBAAsB,CAAC,OAAO,EAAE,mBAAmB;IAInD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB/D,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAMjD,UAAU,IAAI,OAAO;IAIrB,OAAO,CAAC,GAAG,EAAE,KAAK;IAUlB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,UAAU;CAyBnB"}
@@ -11,14 +11,16 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
11
11
  export class JsonRpcStdioClient {
12
12
  proc;
13
13
  requestTimeoutMs;
14
+ killProcess;
14
15
  nextId = 1;
15
16
  pending = new Map();
16
17
  buffer = "";
17
18
  onNotification = null;
18
19
  disposed = false;
19
- constructor(proc, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
20
+ constructor(proc, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, killProcess) {
20
21
  this.proc = proc;
21
22
  this.requestTimeoutMs = requestTimeoutMs;
23
+ this.killProcess = killProcess;
22
24
  proc.stdout.setEncoding("utf8");
23
25
  proc.stdout.on("data", (chunk) => this.ingest(chunk));
24
26
  proc.once("close", () => this.dispose(new Error("app-server subprocess closed")));
@@ -78,7 +80,12 @@ export class JsonRpcStdioClient {
78
80
  killUnhealthy(err) {
79
81
  this.dispose(err);
80
82
  if (this.proc.exitCode === null && this.proc.signalCode === null) {
81
- this.proc.kill("SIGTERM");
83
+ if (this.killProcess) {
84
+ this.killProcess(this.proc);
85
+ }
86
+ else {
87
+ this.proc.kill("SIGTERM");
88
+ }
82
89
  }
83
90
  }
84
91
  ingest(chunk) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/codex-agent",
3
- "version": "1.28.1",
3
+ "version": "1.29.1",
4
4
  "description": "Codex CLI bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "smol-toml": "^1.6.1",
29
- "@parall/agent-core": "1.28.1",
30
- "@parall/cli": "1.28.1",
31
- "@parall/sdk": "1.28.1"
29
+ "@parall/agent-core": "1.29.1",
30
+ "@parall/cli": "1.29.1",
31
+ "@parall/sdk": "1.29.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { execSync } from "node:child_process";
1
+ import { execSync, spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import * as fs from "node:fs";
5
4
  import {
@@ -58,12 +57,28 @@ type TurnEventEnvelope =
58
57
  * main + fork can interleave turns on the same stdio pipe. We route
59
58
  * notifications by threadId the server stamps on every item/turn event.
60
59
  */
60
+ const IS_WIN32 = process.platform === "win32";
61
+
62
+ function quoteWin32Arg(arg: string): string {
63
+ if (!/[\s"&|^<>()]/.test(arg)) return arg;
64
+ return `"${arg.replace(/"/g, '""')}"`;
65
+ }
66
+
67
+ function killWin32Tree(pid: number): boolean {
68
+ try {
69
+ execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
70
+ return true;
71
+ } catch { return false; }
72
+ }
73
+
61
74
  export class CodexAppServerAdapter implements DispatchAdapter {
62
75
  private client: JsonRpcStdioClient | null = null;
63
76
  private proc: ChildProcessWithoutNullStreams | null = null;
64
77
  private initialized = false;
65
78
  private startPromise: Promise<void> | null = null;
66
79
  private readonly activeTurns = new Map<string, TurnSink>();
80
+ private readonly activeTurnIds = new Map<string, string>();
81
+ private readonly pendingInjections = new Map<string, number>();
67
82
  private readonly resumedThreadIds = new Set<string>();
68
83
  private stopping = false;
69
84
 
@@ -94,7 +109,52 @@ export class CodexAppServerAdapter implements DispatchAdapter {
94
109
  if (config.reasoningEffort !== undefined) this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
95
110
  }
96
111
 
112
+ async enqueueDuringDispatch(sessionKey: string, body: string): Promise<boolean> {
113
+ const client = this.client;
114
+ if (!client || client.isDisposed()) return false;
115
+ const threadId = this.opts.sessionManager.getThreadId(sessionKey);
116
+ if (!threadId) return false;
117
+ const turnId = this.activeTurnIds.get(threadId);
118
+ if (!turnId) return false;
119
+ try {
120
+ await client.sendRequest("turn/steer", {
121
+ threadId,
122
+ expectedTurnId: turnId,
123
+ input: buildTurnInput(body, []),
124
+ });
125
+ this.pendingInjections.set(sessionKey, (this.pendingInjections.get(sessionKey) ?? 0) + 1);
126
+ return true;
127
+ } catch (err) {
128
+ this.opts.log?.warn?.(`codex-agent: turn/steer failed: ${errToString(err)}`);
129
+ return false;
130
+ }
131
+ }
132
+
133
+ hasPendingInjections(sessionKey: string): boolean {
134
+ return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
135
+ }
136
+
97
137
  async *dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent> {
138
+ const pending = this.pendingInjections.get(sessionKey) ?? 0;
139
+ if (pending > 0) {
140
+ this.pendingInjections.delete(sessionKey);
141
+ const threadId = this.opts.sessionManager.getThreadId(sessionKey);
142
+ if (threadId && this.client && !this.client.isDisposed()) {
143
+ (this.opts.log ?? context.log)?.info?.(
144
+ `codex-agent[${context.accountId}]: ${pending} steer injection(s) already sent; skipping turn/start`,
145
+ );
146
+ yield {
147
+ type: "runtime_session",
148
+ runtimeSessionId: threadId,
149
+ runtimeLaneKey: sessionKey,
150
+ };
151
+ return;
152
+ }
153
+ (this.opts.log ?? context.log)?.warn?.(
154
+ `codex-agent[${context.accountId}]: pending steer invalidated (subprocess died); falling through to normal dispatch`,
155
+ );
156
+ }
157
+
98
158
  await this.ensureStarted(context.log);
99
159
  // After ensureStarted resolves the subprocess could still die before we
100
160
  // capture the client (handleSubprocessClose nulls this.client). Yield a
@@ -247,7 +307,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
247
307
  threadId = freshThreadId;
248
308
  }
249
309
 
250
- void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
310
+ const turnId = extractTurnId(turnStartResult);
311
+ if (turnId) this.activeTurnIds.set(threadId, turnId);
251
312
  yield {
252
313
  type: "runtime_session",
253
314
  runtimeSessionId: threadId,
@@ -287,8 +348,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
287
348
  } finally {
288
349
  releasePreparedAttachments();
289
350
  this.activeTurns.delete(threadId!);
351
+ this.activeTurnIds.delete(threadId!);
290
352
  if (!sawTurnEnd) {
291
- // Defensive: if we bailed early, make sure we leave no dangling sink.
292
353
  sink.close();
293
354
  }
294
355
  }
@@ -307,10 +368,18 @@ export class CodexAppServerAdapter implements DispatchAdapter {
307
368
  if (!parentThreadId) return null;
308
369
  const handle = this.opts.sessionManager.createForkSessionKey();
309
370
  try {
310
- const result = await client.sendRequest("thread/fork", {
371
+ const forkParams: Record<string, unknown> = {
311
372
  threadId: parentThreadId,
312
373
  ephemeral: true,
313
- });
374
+ };
375
+ if (this.opts.useParallProvider) {
376
+ forkParams.modelProvider = "parall";
377
+ }
378
+ if (this.opts.model) forkParams.model = this.opts.model;
379
+ if (this.opts.reasoningEffort) {
380
+ forkParams.config = { modelReasoningEffort: this.opts.reasoningEffort };
381
+ }
382
+ const result = await client.sendRequest("thread/fork", forkParams);
314
383
  const forkedThreadId = extractThreadId(result);
315
384
  if (!forkedThreadId) {
316
385
  this.opts.log?.warn?.("codex-agent: thread/fork returned no thread id");
@@ -339,9 +408,13 @@ export class CodexAppServerAdapter implements DispatchAdapter {
339
408
  this.proc = null;
340
409
  this.client = null;
341
410
  this.initialized = false;
411
+ this.activeTurnIds.clear();
412
+ this.pendingInjections.clear();
342
413
  if (client) client.dispose(new Error("adapter stopped"));
343
414
  if (proc && proc.exitCode === null && proc.signalCode === null) {
344
- proc.kill("SIGTERM");
415
+ if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
416
+ proc.kill("SIGTERM");
417
+ }
345
418
  }
346
419
  }
347
420
 
@@ -352,6 +425,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
352
425
  activeSink.close();
353
426
  }
354
427
  this.activeTurns.clear();
428
+ this.activeTurnIds.clear();
429
+ this.pendingInjections.clear();
355
430
  this.client = null;
356
431
  this.proc = null;
357
432
  this.initialized = false;
@@ -393,10 +468,13 @@ export class CodexAppServerAdapter implements DispatchAdapter {
393
468
  }
394
469
  const args = ["app-server", "--listen", "stdio://"];
395
470
  (log ?? this.opts.log)?.info?.(`codex-agent: spawning ${this.opts.codexBin} ${args.join(" ")}`);
396
- const proc = spawn(this.opts.codexBin, args, {
471
+ const proc = spawn(
472
+ IS_WIN32 ? quoteWin32Arg(this.opts.codexBin) : this.opts.codexBin,
473
+ IS_WIN32 ? args.map(quoteWin32Arg) : args, {
397
474
  cwd: this.opts.workspaceDir,
398
475
  env,
399
476
  stdio: ["pipe", "pipe", "pipe"],
477
+ shell: IS_WIN32,
400
478
  }) as ChildProcessWithoutNullStreams;
401
479
 
402
480
  proc.stderr.setEncoding("utf8");
@@ -409,7 +487,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
409
487
  // start), we kill the half-spawned subprocess and clear local refs here
410
488
  // — leaving them on `this` would leak the orphan process and cause the
411
489
  // next ensureStarted() to spawn a second app-server on top of it.
412
- const client = new JsonRpcStdioClient(proc);
490
+ const client = new JsonRpcStdioClient(proc, undefined, (p) => {
491
+ if (!IS_WIN32 || !p.pid || !killWin32Tree(p.pid)) {
492
+ p.kill("SIGTERM");
493
+ }
494
+ });
413
495
  client.setNotificationHandler((method, params) => this.routeNotification(method, params));
414
496
 
415
497
  proc.once("close", (code, signal) => this.handleSubprocessClose(proc, code, signal, log));
@@ -423,7 +505,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
423
505
  client.sendNotification("initialized", {});
424
506
  } catch (err) {
425
507
  client.dispose(err instanceof Error ? err : new Error(String(err)));
426
- if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGTERM");
508
+ if (proc.exitCode === null && proc.signalCode === null) {
509
+ if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
510
+ proc.kill("SIGTERM");
511
+ }
512
+ }
427
513
  throw err;
428
514
  }
429
515
 
@@ -459,6 +545,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
459
545
  activeSink.close();
460
546
  }
461
547
  this.activeTurns.clear();
548
+ this.activeTurnIds.clear();
549
+ this.pendingInjections.clear();
462
550
  this.resumedThreadIds.clear();
463
551
  this.client = null;
464
552
  this.proc = null;
@@ -531,6 +619,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
531
619
  sink.push({ kind: "runtime", event });
532
620
  }
533
621
  if (method === "turn/completed") {
622
+ this.activeTurnIds.delete(threadId);
534
623
  sink.push({ kind: "turn_end", threadId });
535
624
  }
536
625
  }
@@ -627,6 +716,15 @@ function extractThreadId(result: unknown): string | undefined {
627
716
  return undefined;
628
717
  }
629
718
 
719
+ function extractTurnId(result: unknown): string | undefined {
720
+ if (!result || typeof result !== "object") return undefined;
721
+ const r = result as Record<string, unknown>;
722
+ if (typeof r.turnId === "string") return r.turnId;
723
+ const turn = r.turn as Record<string, unknown> | undefined;
724
+ if (turn && typeof turn.id === "string") return turn.id;
725
+ return undefined;
726
+ }
727
+
630
728
  function extractThreadIdFromNotification(params: unknown): string | undefined {
631
729
  if (!params || typeof params !== "object") return undefined;
632
730
  const p = params as Record<string, unknown>;
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import * as os from "node:os";
4
- import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
4
+ import { ParallAgentGateway, createPlatformConfigManager, createLogger, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
5
5
  import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
6
6
  import {
7
7
  buildCodexRuntimeKey,
@@ -15,13 +15,7 @@ import { CodexAppServerAdapter } from "./dispatch.js";
15
15
  import { CodexSessionManager } from "./session-manager.js";
16
16
  import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
17
17
 
18
- function createLogger(prefix: string) {
19
- return {
20
- info: (msg: string) => console.log(`[${prefix}] ${msg}`),
21
- warn: (msg: string) => console.warn(`[${prefix}] ${msg}`),
22
- error: (msg: string) => console.error(`[${prefix}] ${msg}`),
23
- };
24
- }
18
+ const log = createLogger("codex-agent");
25
19
 
26
20
  async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string) {
27
21
  try {
@@ -37,7 +31,6 @@ async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string)
37
31
 
38
32
  async function main() {
39
33
  const config = resolveCodexAgentConfig(process.env);
40
- const log = createLogger("codex-agent");
41
34
 
42
35
  const client = new ParallClient({
43
36
  baseUrl: config.apiUrl,
@@ -156,6 +149,6 @@ async function main() {
156
149
  }
157
150
 
158
151
  main().catch((err) => {
159
- console.error(`[codex-agent] fatal: ${String(err)}`);
152
+ log.error(`fatal: ${String(err)}`);
160
153
  process.exitCode = 1;
161
154
  });
@@ -51,6 +51,7 @@ export class JsonRpcStdioClient {
51
51
  constructor(
52
52
  private readonly proc: ChildProcessWithoutNullStreams,
53
53
  private readonly requestTimeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
54
+ private readonly killProcess?: (proc: ChildProcessWithoutNullStreams) => void,
54
55
  ) {
55
56
  proc.stdout.setEncoding("utf8");
56
57
  proc.stdout.on("data", (chunk: string) => this.ingest(chunk));
@@ -114,7 +115,11 @@ export class JsonRpcStdioClient {
114
115
  private killUnhealthy(err: Error) {
115
116
  this.dispose(err);
116
117
  if (this.proc.exitCode === null && this.proc.signalCode === null) {
117
- this.proc.kill("SIGTERM");
118
+ if (this.killProcess) {
119
+ this.killProcess(this.proc);
120
+ } else {
121
+ this.proc.kill("SIGTERM");
122
+ }
118
123
  }
119
124
  }
120
125