@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/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -35
- package/dist/dispatch.d.ts +6 -5
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +102 -82
- package/dist/event-mapping.d.ts +1 -1
- package/dist/event-mapping.d.ts.map +1 -1
- package/dist/event-mapping.js +102 -82
- package/dist/index.js +169 -114
- package/dist/jsonrpc-client.d.ts +4 -4
- package/dist/jsonrpc-client.d.ts.map +1 -1
- package/dist/jsonrpc-client.js +15 -15
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +7 -5
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +40 -39
- package/package.json +4 -4
- package/src/config.ts +41 -36
- package/src/dispatch.ts +140 -110
- package/src/event-mapping.ts +135 -109
- package/src/index.ts +199 -117
- package/src/jsonrpc-client.ts +22 -20
- package/src/session-manager.ts +10 -6
- package/src/workspace.ts +50 -43
package/src/dispatch.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execSync, spawn, type ChildProcessWithoutNullStreams } from
|
|
2
|
-
import { randomUUID } from
|
|
3
|
-
import * as fs from
|
|
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
|
|
9
|
-
import type { PreparedLocalImage } from
|
|
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
|
|
19
|
-
import type { CodexAgentConfig } from
|
|
20
|
-
import { normalizeApprovalPolicy, normalizeSandbox } from
|
|
21
|
-
import { EventMapper } from
|
|
22
|
-
import { JsonRpcStdioClient } from
|
|
23
|
-
import type { CodexSessionManager } from
|
|
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
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
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:
|
|
43
|
-
| { kind:
|
|
44
|
-
| { kind:
|
|
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 ===
|
|
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:
|
|
69
|
+
execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: 'ignore' });
|
|
70
70
|
return true;
|
|
71
|
-
} catch {
|
|
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:
|
|
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)
|
|
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(
|
|
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({
|
|
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:
|
|
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 {
|
|
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:
|
|
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?.(
|
|
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:
|
|
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(
|
|
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:
|
|
288
|
+
type: 'runtime_session',
|
|
266
289
|
runtimeSessionId: threadId,
|
|
267
290
|
runtimeLaneKey: sessionKey,
|
|
268
291
|
};
|
|
269
|
-
yield { type:
|
|
292
|
+
yield { type: 'error', message: `Codex turn/start failed: ${message}` };
|
|
270
293
|
return;
|
|
271
294
|
}
|
|
272
|
-
log?.warn?.(
|
|
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:
|
|
304
|
+
type: 'runtime_session',
|
|
280
305
|
runtimeSessionId: threadId,
|
|
281
306
|
runtimeLaneKey: sessionKey,
|
|
282
307
|
};
|
|
283
|
-
yield {
|
|
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:
|
|
323
|
+
type: 'runtime_session',
|
|
296
324
|
runtimeSessionId: threadId,
|
|
297
325
|
runtimeLaneKey: sessionKey,
|
|
298
326
|
};
|
|
299
|
-
yield {
|
|
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:
|
|
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 ===
|
|
351
|
+
if (envelope.kind === 'turn_end') {
|
|
321
352
|
sawTurnEnd = true;
|
|
322
353
|
break;
|
|
323
354
|
}
|
|
324
|
-
if (envelope.kind ===
|
|
325
|
-
yield { type:
|
|
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 ===
|
|
360
|
+
if (runtimeEvent.type === 'error') {
|
|
330
361
|
yield runtimeEvent;
|
|
331
362
|
continue;
|
|
332
363
|
}
|
|
333
|
-
if (runtimeEvent.type ===
|
|
364
|
+
if (runtimeEvent.type === 'runtime_session') {
|
|
334
365
|
yield runtimeEvent;
|
|
335
366
|
continue;
|
|
336
367
|
}
|
|
337
|
-
if (runtimeEvent.type ===
|
|
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 =
|
|
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(
|
|
413
|
+
const result = await client.sendRequest('thread/fork', forkParams);
|
|
383
414
|
const forkedThreadId = extractThreadId(result);
|
|
384
415
|
if (!forkedThreadId) {
|
|
385
|
-
this.opts.log?.warn?.(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
464
|
-
NO_COLOR:
|
|
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 = [
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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(
|
|
525
|
+
p.kill('SIGTERM');
|
|
493
526
|
}
|
|
494
527
|
});
|
|
495
528
|
client.setNotificationHandler((method, params) => this.routeNotification(method, params));
|
|
496
529
|
|
|
497
|
-
proc.once(
|
|
498
|
-
proc.once(
|
|
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(
|
|
502
|
-
clientInfo: { name:
|
|
534
|
+
await client.sendRequest('initialize', {
|
|
535
|
+
clientInfo: { name: 'parall-codex-agent', version: '1' },
|
|
503
536
|
capabilities: { experimentalApi: false },
|
|
504
537
|
});
|
|
505
|
-
client.sendNotification(
|
|
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(
|
|
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 ??
|
|
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:
|
|
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 =
|
|
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 ?
|
|
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 ===
|
|
604
|
-
const msg = (params as { message?: unknown })?.message ??
|
|
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:
|
|
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:
|
|
652
|
+
sink.push({ kind: 'runtime', event });
|
|
620
653
|
}
|
|
621
|
-
if (method ===
|
|
654
|
+
if (method === 'turn/completed') {
|
|
622
655
|
this.activeTurnIds.delete(threadId);
|
|
623
|
-
sink.push({ kind:
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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(
|
|
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:
|
|
686
|
-
GIT_AUTHOR_EMAIL:
|
|
687
|
-
GIT_COMMITTER_NAME:
|
|
688
|
-
GIT_COMMITTER_EMAIL:
|
|
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(
|
|
692
|
-
execSync(
|
|
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:
|
|
706
|
-
...images.map((image) => ({ type:
|
|
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 !==
|
|
741
|
+
if (!result || typeof result !== 'object') return undefined;
|
|
712
742
|
const r = result as Record<string, unknown>;
|
|
713
|
-
if (typeof 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 ===
|
|
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 !==
|
|
750
|
+
if (!result || typeof result !== 'object') return undefined;
|
|
721
751
|
const r = result as Record<string, unknown>;
|
|
722
|
-
if (typeof 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 ===
|
|
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 !==
|
|
759
|
+
if (!params || typeof params !== 'object') return undefined;
|
|
730
760
|
const p = params as Record<string, unknown>;
|
|
731
|
-
if (typeof 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 ===
|
|
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 ===
|
|
765
|
+
if (meta && typeof meta.threadId === 'string') return meta.threadId;
|
|
736
766
|
return undefined;
|
|
737
767
|
}
|
|
738
768
|
|