@ai-sdk/harness-codex 1.0.23 → 1.0.25

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.
@@ -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
- new Error('codex bridge did not become ready in time.'),
386
- createExitError: () =>
387
- new Error('codex bridge exited before becoming ready.'),
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 drainRest(proc.stdout);
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
- * Gracefully freeze the active turn at a precise cursor. `channel.suspend`
1032
- * stops processing inbound frames (the cursor stops advancing exactly at
1033
- * the last delivered event), drains what was already dispatched, then
1034
- * closes the host socket with reason `'suspended'` which `wireTurn`'s
1035
- * `onClose` treats as a clean turn end. The bridge keeps the turn running
1036
- * and accumulates events past the cursor for the next slice to replay. The
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',