@parall/codex-agent 1.30.0 → 1.32.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/src/dispatch.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { execSync, spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import * as fs from "node:fs";
1
+ import { execSync, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
4
  import {
5
5
  appendPreparedLocalAttachmentRefs,
6
6
  ensureLocalAttachmentGitExclude,
7
7
  pinLocalAttachmentPaths,
8
- } from "@parall/agent-core/internal/attachment-input";
9
- import type { PreparedLocalImage } from "@parall/agent-core/internal/attachment-input";
8
+ } from '@parall/agent-core/internal/attachment-input';
9
+ import type { PreparedLocalImage } from '@parall/agent-core/internal/attachment-input';
10
10
  import type {
11
11
  CleanupForkOpts,
12
12
  DispatchAdapter,
@@ -15,22 +15,22 @@ import type {
15
15
  ForkSessionHandle,
16
16
  GatewayLogger,
17
17
  RuntimeEvent,
18
- } from "@parall/agent-core";
19
- import type { CodexAgentConfig } from "./config.js";
20
- import { normalizeApprovalPolicy, normalizeSandbox } from "./config.js";
21
- import { EventMapper } from "./event-mapping.js";
22
- import { JsonRpcStdioClient } from "./jsonrpc-client.js";
23
- import type { CodexSessionManager } from "./session-manager.js";
18
+ } from '@parall/agent-core';
19
+ import type { CodexAgentConfig } from './config.js';
20
+ import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
21
+ import { EventMapper } from './event-mapping.js';
22
+ import { JsonRpcStdioClient } from './jsonrpc-client.js';
23
+ import type { CodexSessionManager } from './session-manager.js';
24
24
 
25
25
  type CodexAppServerAdapterOptions = Pick<
26
26
  CodexAgentConfig,
27
- | "approvalPolicy"
28
- | "codexBin"
29
- | "codexHome"
30
- | "model"
31
- | "reasoningEffort"
32
- | "sandbox"
33
- | "workspaceDir"
27
+ | 'approvalPolicy'
28
+ | 'codexBin'
29
+ | 'codexHome'
30
+ | 'model'
31
+ | 'reasoningEffort'
32
+ | 'sandbox'
33
+ | 'workspaceDir'
34
34
  > & {
35
35
  sessionManager: CodexSessionManager;
36
36
  log?: GatewayLogger;
@@ -39,9 +39,9 @@ type CodexAppServerAdapterOptions = Pick<
39
39
  };
40
40
 
41
41
  type TurnEventEnvelope =
42
- | { kind: "runtime"; event: RuntimeEvent }
43
- | { kind: "turn_end"; threadId?: string }
44
- | { kind: "error"; message: string };
42
+ | { kind: 'runtime'; event: RuntimeEvent }
43
+ | { kind: 'turn_end'; threadId?: string }
44
+ | { kind: 'error'; message: string };
45
45
 
46
46
  /**
47
47
  * Bridge driver backed by `codex app-server --listen stdio://`.
@@ -57,7 +57,7 @@ type TurnEventEnvelope =
57
57
  * main + fork can interleave turns on the same stdio pipe. We route
58
58
  * notifications by threadId the server stamps on every item/turn event.
59
59
  */
60
- const IS_WIN32 = process.platform === "win32";
60
+ const IS_WIN32 = process.platform === 'win32';
61
61
 
62
62
  function quoteWin32Arg(arg: string): string {
63
63
  if (!/[\s"&|^<>()]/.test(arg)) return arg;
@@ -66,9 +66,11 @@ function quoteWin32Arg(arg: string): string {
66
66
 
67
67
  function killWin32Tree(pid: number): boolean {
68
68
  try {
69
- execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
69
+ execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: 'ignore' });
70
70
  return true;
71
- } catch { return false; }
71
+ } catch {
72
+ return false;
73
+ }
72
74
  }
73
75
 
74
76
  export class CodexAppServerAdapter implements DispatchAdapter {
@@ -96,7 +98,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
96
98
  (log ?? this.opts.log)?.warn?.(
97
99
  `thread ${threadId} already had an active turn; failing the previous dispatch`,
98
100
  );
99
- existing.push({ kind: "error", message: `thread ${threadId} replaced by concurrent turn` });
101
+ existing.push({ kind: 'error', message: `thread ${threadId} replaced by concurrent turn` });
100
102
  existing.close();
101
103
  }
102
104
  this.activeTurns.set(threadId, sink);
@@ -106,7 +108,8 @@ export class CodexAppServerAdapter implements DispatchAdapter {
106
108
 
107
109
  updateConfig(config: { model?: string | null; reasoningEffort?: string | null }): void {
108
110
  if (config.model !== undefined) this.opts.model = config.model ?? undefined;
109
- if (config.reasoningEffort !== undefined) this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
111
+ if (config.reasoningEffort !== undefined)
112
+ this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
110
113
  }
111
114
 
112
115
  async enqueueDuringDispatch(sessionKey: string, body: string): Promise<boolean> {
@@ -117,7 +120,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
117
120
  const turnId = this.activeTurnIds.get(threadId);
118
121
  if (!turnId) return false;
119
122
  try {
120
- await client.sendRequest("turn/steer", {
123
+ await client.sendRequest('turn/steer', {
121
124
  threadId,
122
125
  expectedTurnId: turnId,
123
126
  input: buildTurnInput(body, []),
@@ -130,11 +133,26 @@ export class CodexAppServerAdapter implements DispatchAdapter {
130
133
  }
131
134
  }
132
135
 
136
+ abortDispatch(sessionKey: string): void {
137
+ this.pendingInjections.delete(sessionKey);
138
+ const threadId = this.opts.sessionManager.getThreadId(sessionKey);
139
+ if (!threadId) return;
140
+ const sink = this.activeTurns.get(threadId);
141
+ if (!sink) return;
142
+ sink.push({ kind: 'error', message: 'dispatch deadline exceeded' });
143
+ sink.close();
144
+ }
145
+
133
146
  hasPendingInjections(sessionKey: string): boolean {
134
147
  return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
135
148
  }
136
149
 
137
- async *dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent> {
150
+ async *dispatch({
151
+ event,
152
+ bodyForAgent,
153
+ sessionKey,
154
+ context,
155
+ }: DispatchOpts): AsyncIterable<RuntimeEvent> {
138
156
  const pending = this.pendingInjections.get(sessionKey) ?? 0;
139
157
  if (pending > 0) {
140
158
  this.pendingInjections.delete(sessionKey);
@@ -144,7 +162,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
144
162
  `${pending} steer injection(s) already sent; skipping turn/start`,
145
163
  );
146
164
  yield {
147
- type: "runtime_session",
165
+ type: 'runtime_session',
148
166
  runtimeSessionId: threadId,
149
167
  runtimeLaneKey: sessionKey,
150
168
  };
@@ -162,7 +180,10 @@ export class CodexAppServerAdapter implements DispatchAdapter {
162
180
  // throw a TypeError on the next sendRequest call.
163
181
  const client = this.client;
164
182
  if (!client) {
165
- yield { type: "error", message: "Codex app-server not available (subprocess died during dispatch start)" };
183
+ yield {
184
+ type: 'error',
185
+ message: 'Codex app-server not available (subprocess died during dispatch start)',
186
+ };
166
187
  return;
167
188
  }
168
189
  const log = this.opts.log ?? context.log;
@@ -181,7 +202,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
181
202
  // fail the resume path and replace the thread, losing the first turn.
182
203
  this.resumedThreadIds.add(threadId);
183
204
  } catch (err) {
184
- yield { type: "error", message: `Codex thread/start failed: ${errToString(err)}` };
205
+ yield { type: 'error', message: `Codex thread/start failed: ${errToString(err)}` };
185
206
  return;
186
207
  }
187
208
  } else if (isMainSession && !this.resumedThreadIds.has(threadId)) {
@@ -195,13 +216,15 @@ export class CodexAppServerAdapter implements DispatchAdapter {
195
216
  this.opts.sessionManager.recordThreadId(sessionKey, threadId);
196
217
  this.resumedThreadIds.add(threadId);
197
218
  } catch (err) {
198
- log?.warn?.(`thread/resume failed (${errToString(err)}); attempting one-shot fresh-thread start`);
219
+ log?.warn?.(
220
+ `thread/resume failed (${errToString(err)}); attempting one-shot fresh-thread start`,
221
+ );
199
222
  let freshThreadId: string;
200
223
  try {
201
224
  freshThreadId = await this.openThread(client, { resumeId: undefined });
202
225
  } catch (innerErr) {
203
226
  yield {
204
- type: "error",
227
+ type: 'error',
205
228
  message: `Codex thread/start failed after resume error (persisted thread retained): ${errToString(innerErr)}`,
206
229
  };
207
230
  return;
@@ -241,7 +264,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
241
264
  // silent fallback that hides real protocol drift.
242
265
  const turnInput = buildTurnInput(preparedBody, preparedImages);
243
266
  const startTurn = (targetThreadId: string): Promise<unknown> =>
244
- client.sendRequest("turn/start", {
267
+ client.sendRequest('turn/start', {
245
268
  threadId: targetThreadId,
246
269
  input: turnInput,
247
270
  });
@@ -262,25 +285,30 @@ export class CodexAppServerAdapter implements DispatchAdapter {
262
285
  // Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
263
286
  if (!isMainSession) {
264
287
  yield {
265
- type: "runtime_session",
288
+ type: 'runtime_session',
266
289
  runtimeSessionId: threadId,
267
290
  runtimeLaneKey: sessionKey,
268
291
  };
269
- yield { type: "error", message: `Codex turn/start failed: ${message}` };
292
+ yield { type: 'error', message: `Codex turn/start failed: ${message}` };
270
293
  return;
271
294
  }
272
- log?.warn?.(`turn/start on thread ${threadId} failed (${message}); attempting one-shot fresh-thread retry`);
295
+ log?.warn?.(
296
+ `turn/start on thread ${threadId} failed (${message}); attempting one-shot fresh-thread retry`,
297
+ );
273
298
  this.activeTurns.delete(threadId);
274
299
  let freshThreadId: string;
275
300
  try {
276
301
  freshThreadId = await this.openThread(client, { resumeId: undefined });
277
302
  } catch (createErr) {
278
303
  yield {
279
- type: "runtime_session",
304
+ type: 'runtime_session',
280
305
  runtimeSessionId: threadId,
281
306
  runtimeLaneKey: sessionKey,
282
307
  };
283
- yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
308
+ yield {
309
+ type: 'error',
310
+ message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}`,
311
+ };
284
312
  return;
285
313
  }
286
314
  this.setActiveTurn(freshThreadId, sink, log);
@@ -292,11 +320,14 @@ export class CodexAppServerAdapter implements DispatchAdapter {
292
320
  // resume again.
293
321
  this.activeTurns.delete(freshThreadId);
294
322
  yield {
295
- type: "runtime_session",
323
+ type: 'runtime_session',
296
324
  runtimeSessionId: threadId,
297
325
  runtimeLaneKey: sessionKey,
298
326
  };
299
- yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
327
+ yield {
328
+ type: 'error',
329
+ message: `Codex turn/start failed after retry: ${errToString(retryErr)}`,
330
+ };
300
331
  return;
301
332
  }
302
333
  // Retry accepted — the original thread really was unusable. Now safe
@@ -310,31 +341,31 @@ export class CodexAppServerAdapter implements DispatchAdapter {
310
341
  const turnId = extractTurnId(turnStartResult);
311
342
  if (turnId) this.activeTurnIds.set(threadId, turnId);
312
343
  yield {
313
- type: "runtime_session",
344
+ type: 'runtime_session',
314
345
  runtimeSessionId: threadId,
315
346
  runtimeLaneKey: sessionKey,
316
347
  };
317
348
 
318
349
  while (true) {
319
350
  const envelope = await sink.next();
320
- if (envelope.kind === "turn_end") {
351
+ if (envelope.kind === 'turn_end') {
321
352
  sawTurnEnd = true;
322
353
  break;
323
354
  }
324
- if (envelope.kind === "error") {
325
- yield { type: "error", message: envelope.message };
355
+ if (envelope.kind === 'error') {
356
+ yield { type: 'error', message: envelope.message };
326
357
  continue;
327
358
  }
328
359
  const runtimeEvent = envelope.event;
329
- if (runtimeEvent.type === "error") {
360
+ if (runtimeEvent.type === 'error') {
330
361
  yield runtimeEvent;
331
362
  continue;
332
363
  }
333
- if (runtimeEvent.type === "runtime_session") {
364
+ if (runtimeEvent.type === 'runtime_session') {
334
365
  yield runtimeEvent;
335
366
  continue;
336
367
  }
337
- if (runtimeEvent.type === "text") {
368
+ if (runtimeEvent.type === 'text') {
338
369
  // Layer 0 symmetric output contract: Codex's plain text is never
339
370
  // projected as a chat message. Outbound messages must come from the
340
371
  // agent explicitly invoking `@parall/cli messages send` / `dm` via
@@ -373,16 +404,16 @@ export class CodexAppServerAdapter implements DispatchAdapter {
373
404
  ephemeral: true,
374
405
  };
375
406
  if (this.opts.useParallProvider) {
376
- forkParams.modelProvider = "parall";
407
+ forkParams.modelProvider = 'parall';
377
408
  }
378
409
  if (this.opts.model) forkParams.model = this.opts.model;
379
410
  if (this.opts.reasoningEffort) {
380
411
  forkParams.config = { modelReasoningEffort: this.opts.reasoningEffort };
381
412
  }
382
- const result = await client.sendRequest("thread/fork", forkParams);
413
+ const result = await client.sendRequest('thread/fork', forkParams);
383
414
  const forkedThreadId = extractThreadId(result);
384
415
  if (!forkedThreadId) {
385
- this.opts.log?.warn?.("thread/fork returned no thread id");
416
+ this.opts.log?.warn?.('thread/fork returned no thread id');
386
417
  return null;
387
418
  }
388
419
  this.opts.sessionManager.recordThreadId(handle.sessionKey, forkedThreadId);
@@ -410,10 +441,10 @@ export class CodexAppServerAdapter implements DispatchAdapter {
410
441
  this.initialized = false;
411
442
  this.activeTurnIds.clear();
412
443
  this.pendingInjections.clear();
413
- if (client) client.dispose(new Error("adapter stopped"));
444
+ if (client) client.dispose(new Error('adapter stopped'));
414
445
  if (proc && proc.exitCode === null && proc.signalCode === null) {
415
446
  if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
416
- proc.kill("SIGTERM");
447
+ proc.kill('SIGTERM');
417
448
  }
418
449
  }
419
450
  }
@@ -421,7 +452,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
421
452
  private async ensureStarted(log?: GatewayLogger): Promise<void> {
422
453
  if (this.client?.isDisposed()) {
423
454
  for (const activeSink of this.activeTurns.values()) {
424
- activeSink.push({ kind: "error", message: "Codex app-server disposed; resetting adapter" });
455
+ activeSink.push({ kind: 'error', message: 'Codex app-server disposed; resetting adapter' });
425
456
  activeSink.close();
426
457
  }
427
458
  this.activeTurns.clear();
@@ -460,25 +491,27 @@ export class CodexAppServerAdapter implements DispatchAdapter {
460
491
  const env: Record<string, string | undefined> = {
461
492
  ...process.env,
462
493
  CODEX_HOME: this.opts.codexHome,
463
- FORCE_COLOR: "0",
464
- NO_COLOR: "1",
494
+ FORCE_COLOR: '0',
495
+ NO_COLOR: '1',
465
496
  };
466
497
  if (this.opts.contextFilePath) {
467
498
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
468
499
  }
469
- const args = ["app-server", "--listen", "stdio://"];
470
- (log ?? this.opts.log)?.info?.(`spawning ${this.opts.codexBin} ${args.join(" ")}`);
500
+ const args = ['app-server', '--listen', 'stdio://'];
501
+ (log ?? this.opts.log)?.info?.(`spawning ${this.opts.codexBin} ${args.join(' ')}`);
471
502
  const proc = spawn(
472
503
  IS_WIN32 ? quoteWin32Arg(this.opts.codexBin) : this.opts.codexBin,
473
- IS_WIN32 ? args.map(quoteWin32Arg) : args, {
474
- cwd: this.opts.workspaceDir,
475
- env,
476
- stdio: ["pipe", "pipe", "pipe"],
477
- shell: IS_WIN32,
478
- }) as ChildProcessWithoutNullStreams;
479
-
480
- proc.stderr.setEncoding("utf8");
481
- proc.stderr.on("data", (chunk: string) => {
504
+ IS_WIN32 ? args.map(quoteWin32Arg) : args,
505
+ {
506
+ cwd: this.opts.workspaceDir,
507
+ env,
508
+ stdio: ['pipe', 'pipe', 'pipe'],
509
+ shell: IS_WIN32,
510
+ },
511
+ ) as ChildProcessWithoutNullStreams;
512
+
513
+ proc.stderr.setEncoding('utf8');
514
+ proc.stderr.on('data', (chunk: string) => {
482
515
  (log ?? this.opts.log)?.warn?.(`[stderr] ${chunk.trim()}`);
483
516
  });
484
517
 
@@ -489,25 +522,25 @@ export class CodexAppServerAdapter implements DispatchAdapter {
489
522
  // next ensureStarted() to spawn a second app-server on top of it.
490
523
  const client = new JsonRpcStdioClient(proc, undefined, (p) => {
491
524
  if (!IS_WIN32 || !p.pid || !killWin32Tree(p.pid)) {
492
- p.kill("SIGTERM");
525
+ p.kill('SIGTERM');
493
526
  }
494
527
  });
495
528
  client.setNotificationHandler((method, params) => this.routeNotification(method, params));
496
529
 
497
- proc.once("close", (code, signal) => this.handleSubprocessClose(proc, code, signal, log));
498
- proc.once("error", (err) => this.handleSubprocessClose(proc, null, null, log, err));
530
+ proc.once('close', (code, signal) => this.handleSubprocessClose(proc, code, signal, log));
531
+ proc.once('error', (err) => this.handleSubprocessClose(proc, null, null, log, err));
499
532
 
500
533
  try {
501
- await client.sendRequest("initialize", {
502
- clientInfo: { name: "parall-codex-agent", version: "1" },
534
+ await client.sendRequest('initialize', {
535
+ clientInfo: { name: 'parall-codex-agent', version: '1' },
503
536
  capabilities: { experimentalApi: false },
504
537
  });
505
- client.sendNotification("initialized", {});
538
+ client.sendNotification('initialized', {});
506
539
  } catch (err) {
507
540
  client.dispose(err instanceof Error ? err : new Error(String(err)));
508
541
  if (proc.exitCode === null && proc.signalCode === null) {
509
542
  if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
510
- proc.kill("SIGTERM");
543
+ proc.kill('SIGTERM');
511
544
  }
512
545
  }
513
546
  throw err;
@@ -532,7 +565,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
532
565
  if (this.proc !== null && this.proc !== proc) return;
533
566
  const reason = err
534
567
  ? `spawn error: ${err.message}`
535
- : `exited (code=${code ?? "null"}${signal ? `, signal=${signal}` : ""})`;
568
+ : `exited (code=${code ?? 'null'}${signal ? `, signal=${signal}` : ''})`;
536
569
  const logger = log ?? this.opts.log;
537
570
  if (this.stopping) {
538
571
  logger?.info?.(`app-server subprocess ${reason} during graceful stop`);
@@ -541,7 +574,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
541
574
  }
542
575
  this.client?.dispose(err ?? new Error(`app-server ${reason}`));
543
576
  for (const activeSink of this.activeTurns.values()) {
544
- activeSink.push({ kind: "error", message: `Codex app-server ${reason}` });
577
+ activeSink.push({ kind: 'error', message: `Codex app-server ${reason}` });
545
578
  activeSink.close();
546
579
  }
547
580
  this.activeTurns.clear();
@@ -570,7 +603,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
570
603
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
571
604
  };
572
605
  if (this.opts.useParallProvider) {
573
- commonParams.modelProvider = "parall";
606
+ commonParams.modelProvider = 'parall';
574
607
  }
575
608
  commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
576
609
  if (this.opts.model) commonParams.model = this.opts.model;
@@ -583,7 +616,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
583
616
  commonParams.config = { modelReasoningEffort: this.opts.reasoningEffort };
584
617
  }
585
618
 
586
- const method = opts.resumeId ? "thread/resume" : "thread/start";
619
+ const method = opts.resumeId ? 'thread/resume' : 'thread/start';
587
620
  const params: Record<string, unknown> = opts.resumeId
588
621
  ? { threadId: opts.resumeId, ...commonParams }
589
622
  : { cwd: this.opts.workspaceDir, ...commonParams };
@@ -600,10 +633,10 @@ export class CodexAppServerAdapter implements DispatchAdapter {
600
633
  const threadId = extractThreadIdFromNotification(params);
601
634
  if (!threadId) {
602
635
  // Surface server-initiated generic errors to every active turn.
603
- if (method === "error") {
604
- const msg = (params as { message?: unknown })?.message ?? "Codex app-server error";
636
+ if (method === 'error') {
637
+ const msg = (params as { message?: unknown })?.message ?? 'Codex app-server error';
605
638
  for (const sink of this.activeTurns.values()) {
606
- sink.push({ kind: "error", message: String(msg) });
639
+ sink.push({ kind: 'error', message: String(msg) });
607
640
  }
608
641
  }
609
642
  return;
@@ -616,14 +649,13 @@ export class CodexAppServerAdapter implements DispatchAdapter {
616
649
  // RuntimeEvent error if `turn.status === "failed"`. Enqueue those events
617
650
  // before the turn_end sentinel so the dispatch loop can yield them.
618
651
  for (const event of sink.mapper.map(method, params)) {
619
- sink.push({ kind: "runtime", event });
652
+ sink.push({ kind: 'runtime', event });
620
653
  }
621
- if (method === "turn/completed") {
654
+ if (method === 'turn/completed') {
622
655
  this.activeTurnIds.delete(threadId);
623
- sink.push({ kind: "turn_end", threadId });
656
+ sink.push({ kind: 'turn_end', threadId });
624
657
  }
625
658
  }
626
-
627
659
  }
628
660
 
629
661
  /** Per-turn buffered sink backed by an unbounded promise queue. */
@@ -652,7 +684,7 @@ class TurnSink {
652
684
  const pending = this.queue.shift();
653
685
  if (pending) return Promise.resolve(pending);
654
686
  if (this.closed) {
655
- return Promise.resolve({ kind: "turn_end" });
687
+ return Promise.resolve({ kind: 'turn_end' });
656
688
  }
657
689
  return new Promise((resolve) => {
658
690
  this.resolver = resolve;
@@ -663,7 +695,7 @@ class TurnSink {
663
695
  this.closed = true;
664
696
  const r = this.resolver;
665
697
  this.resolver = null;
666
- r?.({ kind: "turn_end" });
698
+ r?.({ kind: 'turn_end' });
667
699
  }
668
700
  }
669
701
 
@@ -671,10 +703,10 @@ function ensureGitRepo(workingDirectory: string): void {
671
703
  fs.mkdirSync(workingDirectory, { recursive: true });
672
704
  // Only `git init` if the workspace isn't already inside any git repo. A
673
705
  // bare existsSync(.git) check would miss the common case of a user pointing
674
- // PRLL_CODEX_WORKSPACE_DIR at a subdirectory of their existing project,
706
+ // PRLL_WORKSPACE_DIR at a subdirectory of their existing project,
675
707
  // and silently creating a nested repo there would mangle their layout.
676
708
  try {
677
- execSync("git rev-parse --is-inside-work-tree", { cwd: workingDirectory, stdio: "pipe" });
709
+ execSync('git rev-parse --is-inside-work-tree', { cwd: workingDirectory, stdio: 'pipe' });
678
710
  ensureLocalAttachmentGitExclude(workingDirectory);
679
711
  return;
680
712
  } catch {
@@ -682,57 +714,55 @@ function ensureGitRepo(workingDirectory: string): void {
682
714
  }
683
715
  const env = {
684
716
  ...process.env,
685
- GIT_AUTHOR_NAME: "parall-codex-agent",
686
- GIT_AUTHOR_EMAIL: "agent@parall.local",
687
- GIT_COMMITTER_NAME: "parall-codex-agent",
688
- GIT_COMMITTER_EMAIL: "agent@parall.local",
717
+ GIT_AUTHOR_NAME: 'parall-codex-agent',
718
+ GIT_AUTHOR_EMAIL: 'agent@parall.local',
719
+ GIT_COMMITTER_NAME: 'parall-codex-agent',
720
+ GIT_COMMITTER_EMAIL: 'agent@parall.local',
689
721
  };
690
722
  try {
691
- execSync("git init", { cwd: workingDirectory, stdio: "pipe", env });
692
- execSync("git commit --allow-empty -m init", { cwd: workingDirectory, stdio: "pipe", env });
723
+ execSync('git init', { cwd: workingDirectory, stdio: 'pipe', env });
724
+ execSync('git commit --allow-empty -m init', { cwd: workingDirectory, stdio: 'pipe', env });
693
725
  ensureLocalAttachmentGitExclude(workingDirectory);
694
726
  } catch {
695
727
  // Non-fatal: codex app-server may still accept a bare directory. Let it raise at turn time.
696
728
  }
697
729
  }
698
730
 
699
- type CodexTurnInput =
700
- | { type: "text"; text: string }
701
- | { type: "localImage"; path: string };
731
+ type CodexTurnInput = { type: 'text'; text: string } | { type: 'localImage'; path: string };
702
732
 
703
733
  function buildTurnInput(body: string, images: PreparedLocalImage[]): CodexTurnInput[] {
704
734
  return [
705
- { type: "text", text: body },
706
- ...images.map((image) => ({ type: "localImage" as const, path: image.localPath })),
735
+ { type: 'text', text: body },
736
+ ...images.map((image) => ({ type: 'localImage' as const, path: image.localPath })),
707
737
  ];
708
738
  }
709
739
 
710
740
  function extractThreadId(result: unknown): string | undefined {
711
- if (!result || typeof result !== "object") return undefined;
741
+ if (!result || typeof result !== 'object') return undefined;
712
742
  const r = result as Record<string, unknown>;
713
- if (typeof r.threadId === "string") return r.threadId;
743
+ if (typeof r.threadId === 'string') return r.threadId;
714
744
  const thread = r.thread as Record<string, unknown> | undefined;
715
- if (thread && typeof thread.id === "string") return thread.id;
745
+ if (thread && typeof thread.id === 'string') return thread.id;
716
746
  return undefined;
717
747
  }
718
748
 
719
749
  function extractTurnId(result: unknown): string | undefined {
720
- if (!result || typeof result !== "object") return undefined;
750
+ if (!result || typeof result !== 'object') return undefined;
721
751
  const r = result as Record<string, unknown>;
722
- if (typeof r.turnId === "string") return r.turnId;
752
+ if (typeof r.turnId === 'string') return r.turnId;
723
753
  const turn = r.turn as Record<string, unknown> | undefined;
724
- if (turn && typeof turn.id === "string") return turn.id;
754
+ if (turn && typeof turn.id === 'string') return turn.id;
725
755
  return undefined;
726
756
  }
727
757
 
728
758
  function extractThreadIdFromNotification(params: unknown): string | undefined {
729
- if (!params || typeof params !== "object") return undefined;
759
+ if (!params || typeof params !== 'object') return undefined;
730
760
  const p = params as Record<string, unknown>;
731
- if (typeof p.threadId === "string") return p.threadId;
761
+ if (typeof p.threadId === 'string') return p.threadId;
732
762
  const thread = p.thread as Record<string, unknown> | undefined;
733
- if (thread && typeof thread.id === "string") return thread.id;
763
+ if (thread && typeof thread.id === 'string') return thread.id;
734
764
  const meta = (p._meta ?? p.meta) as Record<string, unknown> | undefined;
735
- if (meta && typeof meta.threadId === "string") return meta.threadId;
765
+ if (meta && typeof meta.threadId === 'string') return meta.threadId;
736
766
  return undefined;
737
767
  }
738
768