@ai-sdk/harness-codex 1.0.22 → 1.0.24
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/CHANGELOG.md +18 -0
- package/dist/bridge/index.mjs +144 -31
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +35 -32
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/codex-step-tracker.ts +83 -0
- package/src/bridge/index.ts +36 -27
- package/src/codex-harness.ts +42 -52
package/src/codex-harness.ts
CHANGED
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
} from '@ai-sdk/harness';
|
|
23
23
|
import {
|
|
24
24
|
classifyDiskLog,
|
|
25
|
+
createBridgeErrorHandler,
|
|
26
|
+
createBridgeStartupError,
|
|
27
|
+
drainBridgeProcessStream,
|
|
28
|
+
forwardBridgeProcessStream,
|
|
25
29
|
markBridgeStarting,
|
|
26
30
|
resolveSandboxHomeDir,
|
|
27
31
|
SandboxChannel,
|
|
@@ -258,6 +262,10 @@ export function createCodex(
|
|
|
258
262
|
}),
|
|
259
263
|
)
|
|
260
264
|
: undefined;
|
|
265
|
+
const onBridgeError = createBridgeErrorHandler({
|
|
266
|
+
harnessId: 'codex',
|
|
267
|
+
sessionId: startOpts.sessionId,
|
|
268
|
+
});
|
|
261
269
|
|
|
262
270
|
/*
|
|
263
271
|
* Rung 1 — ATTACH. With live coordinates, reopen a socket to the
|
|
@@ -279,6 +287,7 @@ export function createCodex(
|
|
|
279
287
|
outboundSchema: outboundMessageSchema,
|
|
280
288
|
initialLastSeenEventId: coords.lastSeenEventId,
|
|
281
289
|
onDiagnostic,
|
|
290
|
+
onBridgeError,
|
|
282
291
|
});
|
|
283
292
|
await attachChannel.open(isContinue ? { resume: true } : undefined);
|
|
284
293
|
return createSession({
|
|
@@ -373,6 +382,13 @@ export function createCodex(
|
|
|
373
382
|
env,
|
|
374
383
|
abortSignal: startOpts.abortSignal,
|
|
375
384
|
});
|
|
385
|
+
const stderrTail: string[] = [];
|
|
386
|
+
const bridgeStderrDone = forwardBridgeProcessStream({
|
|
387
|
+
stream: proc.stderr,
|
|
388
|
+
streamName: 'stderr',
|
|
389
|
+
source: 'codex',
|
|
390
|
+
collectTail: stderrTail,
|
|
391
|
+
});
|
|
376
392
|
|
|
377
393
|
const { port: boundPort } = await waitForBridgeReady({
|
|
378
394
|
proc,
|
|
@@ -381,22 +397,24 @@ export function createCodex(
|
|
|
381
397
|
bridgeType: 'codex',
|
|
382
398
|
timeoutMs,
|
|
383
399
|
abortSignal: startOpts.abortSignal,
|
|
384
|
-
createTimeoutError: () =>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
createTimeoutError: ({ proc, stdoutTail }) =>
|
|
401
|
+
createBridgeStartupError({
|
|
402
|
+
message: 'codex bridge did not become ready in time.',
|
|
403
|
+
proc,
|
|
404
|
+
stdoutTail,
|
|
405
|
+
stderrTail,
|
|
406
|
+
stderrDone: bridgeStderrDone,
|
|
407
|
+
}),
|
|
408
|
+
createExitError: ({ proc, stdoutTail }) =>
|
|
409
|
+
createBridgeStartupError({
|
|
410
|
+
message: 'codex bridge exited before becoming ready.',
|
|
411
|
+
proc,
|
|
412
|
+
stdoutTail,
|
|
413
|
+
stderrTail,
|
|
414
|
+
stderrDone: bridgeStderrDone,
|
|
415
|
+
}),
|
|
388
416
|
});
|
|
389
|
-
void
|
|
390
|
-
/*
|
|
391
|
-
* Bridge stderr is the only diagnostic channel for what happens
|
|
392
|
-
* inside the sandbox once the bridge is running (uncaught
|
|
393
|
-
* exceptions, Codex SDK errors, network failures). Forward it
|
|
394
|
-
* line-by-line to the host console so a mid-turn bridge crash can
|
|
395
|
-
* be inspected from `pnpm dev` logs without redeploying. The
|
|
396
|
-
* bridge itself writes nothing to stderr in steady state, so this
|
|
397
|
-
* is silent on the happy path.
|
|
398
|
-
*/
|
|
399
|
-
void forwardBridgeStderr(proc.stderr);
|
|
417
|
+
void drainBridgeProcessStream(proc.stdout);
|
|
400
418
|
|
|
401
419
|
const wsUrl =
|
|
402
420
|
(await sandboxSession.getPortUrl({
|
|
@@ -408,6 +426,7 @@ export function createCodex(
|
|
|
408
426
|
connect: () => openWebSocket(wsUrl),
|
|
409
427
|
outboundSchema: outboundMessageSchema,
|
|
410
428
|
onDiagnostic,
|
|
429
|
+
onBridgeError,
|
|
411
430
|
// In replay mode the respawned bridge reloaded the finished turn from
|
|
412
431
|
// disk; seed the cursor and resume so it streams the tail (incl.
|
|
413
432
|
// `finish`).
|
|
@@ -506,37 +525,6 @@ async function writeCodexSkills({
|
|
|
506
525
|
};
|
|
507
526
|
}
|
|
508
527
|
|
|
509
|
-
async function forwardBridgeStderr(
|
|
510
|
-
stream: ReadableStream<Uint8Array>,
|
|
511
|
-
): Promise<void> {
|
|
512
|
-
try {
|
|
513
|
-
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
|
|
514
|
-
while (true) {
|
|
515
|
-
const { value, done } = await reader.read();
|
|
516
|
-
if (done) return;
|
|
517
|
-
if (value) {
|
|
518
|
-
const trimmed = value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
519
|
-
if (trimmed.length > 0) {
|
|
520
|
-
// eslint-disable-next-line no-console
|
|
521
|
-
console.log(`[bridge stderr] ${trimmed}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
} catch {
|
|
526
|
-
// Reader errors are non-fatal — best-effort diagnostic only.
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
async function drainRest(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
531
|
-
try {
|
|
532
|
-
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
|
|
533
|
-
while (true) {
|
|
534
|
-
const { done } = await reader.read();
|
|
535
|
-
if (done) return;
|
|
536
|
-
}
|
|
537
|
-
} catch {}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
528
|
function openWebSocket(url: string): Promise<WebSocket> {
|
|
541
529
|
return new Promise((resolve, reject) => {
|
|
542
530
|
const ws = new WebSocket(url);
|
|
@@ -1028,14 +1016,16 @@ function createSession({
|
|
|
1028
1016
|
}
|
|
1029
1017
|
stopped = true;
|
|
1030
1018
|
/*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1019
|
+
* First ask the runtime to interrupt the active model turn, then freeze
|
|
1020
|
+
* the host at a precise cursor. `channel.suspend` stops processing
|
|
1021
|
+
* inbound frames (the cursor stops advancing exactly at the last
|
|
1022
|
+
* delivered event), drains what was already dispatched, then closes the
|
|
1023
|
+
* host socket with reason `'suspended'` — which `wireTurn`'s `onClose`
|
|
1024
|
+
* treats as a clean turn end. The bridge keeps the turn running and
|
|
1025
|
+
* accumulates events past the cursor for the next slice to replay. The
|
|
1037
1026
|
* sandbox process is deliberately left alive (no `shutdown`/`detach`).
|
|
1038
1027
|
*/
|
|
1028
|
+
await channel.interrupt();
|
|
1039
1029
|
const lastSeenEventId = await channel.suspend();
|
|
1040
1030
|
const payload: HarnessV1ContinueTurnState = {
|
|
1041
1031
|
type: 'continue-turn',
|