codex-copilot-dx 0.7.7 → 0.7.9

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.
@@ -0,0 +1,100 @@
1
+ import { terminalCell } from "./cli-table.mjs";
2
+
3
+ const DEFAULT_RECENT_FAILURES = 10;
4
+
5
+ function safeIdentifier(value, fallback = "unknown") {
6
+ const text = terminalCell(value, { fallback });
7
+ return text.slice(0, 200);
8
+ }
9
+
10
+ function safeFailureMessage(value) {
11
+ let text = terminalCell(value, { fallback: "Response failed without an error message" });
12
+ text = text
13
+ .replace(/(encrypted(?: function output)? content\s+)[A-Za-z0-9+/_=-]{24,}/gi, "$1[redacted]")
14
+ .replace(/\b[A-Za-z0-9+/_=-]{96,}\b/g, "[redacted]");
15
+ return text.slice(0, 500);
16
+ }
17
+
18
+ function upstreamRequestId(headers) {
19
+ if (!headers?.get) return null;
20
+ for (const name of ["x-request-id", "x-github-request-id", "x-ms-request-id", "request-id", "x-correlation-id"]) {
21
+ const value = headers.get(name);
22
+ if (value) return safeIdentifier(value, "");
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function responseFailureDetails(event, eventType = event?.type, {
28
+ model,
29
+ headers,
30
+ retried = false,
31
+ retryPolicy = null,
32
+ retrySkipped = null,
33
+ } = {}) {
34
+ const response = event?.response && typeof event.response === "object" ? event.response : event;
35
+ const error = response?.error && typeof response.error === "object"
36
+ ? response.error
37
+ : event?.error && typeof event.error === "object"
38
+ ? event.error
39
+ : event;
40
+ return {
41
+ at: new Date().toISOString(),
42
+ event_type: safeIdentifier(eventType || "response.failed"),
43
+ model: safeIdentifier(response?.model || model),
44
+ code: safeIdentifier(error?.code || "unknown_error"),
45
+ message: safeFailureMessage(error?.message),
46
+ response_id: response?.id ? safeIdentifier(response.id, "") : null,
47
+ upstream_request_id: upstreamRequestId(headers),
48
+ retried: Boolean(retried),
49
+ retry_policy: retryPolicy ? safeIdentifier(retryPolicy, "") : null,
50
+ retry_skipped: retrySkipped ? safeIdentifier(retrySkipped, "") : null,
51
+ };
52
+ }
53
+
54
+ export function responseFailureText(event, eventType = event?.type) {
55
+ const response = event?.response && typeof event.response === "object" ? event.response : event;
56
+ const error = response?.error && typeof response.error === "object"
57
+ ? response.error
58
+ : event?.error && typeof event.error === "object"
59
+ ? event.error
60
+ : event;
61
+ return JSON.stringify({ error: { code: error?.code, message: error?.message } });
62
+ }
63
+
64
+ export function formatResponseFailureLog(details) {
65
+ const fields = [
66
+ `Responses ${details.event_type}`,
67
+ `model=${details.model}`,
68
+ `code=${details.code}`,
69
+ details.response_id ? `response_id=${details.response_id}` : null,
70
+ details.upstream_request_id ? `upstream_request_id=${details.upstream_request_id}` : null,
71
+ details.retried ? `retry=${details.retry_policy || "yes"}` : null,
72
+ details.retry_skipped ? `retry_skipped=${details.retry_skipped}` : null,
73
+ `message=${details.message}`,
74
+ ].filter(Boolean);
75
+ return fields.join(" ");
76
+ }
77
+
78
+ export function createResponseFailureDiagnostics({
79
+ maxEntries = DEFAULT_RECENT_FAILURES,
80
+ now = () => new Date().toISOString(),
81
+ } = {}) {
82
+ const recent = [];
83
+ let total = 0;
84
+ let retried = 0;
85
+ return {
86
+ record(details) {
87
+ total += 1;
88
+ if (details.retried) retried += 1;
89
+ recent.push({ ...details, at: now() });
90
+ if (recent.length > maxEntries) recent.splice(0, recent.length - maxEntries);
91
+ },
92
+ snapshot() {
93
+ return {
94
+ total,
95
+ retried,
96
+ recent: recent.map((entry) => ({ ...entry })),
97
+ };
98
+ },
99
+ };
100
+ }
@@ -109,6 +109,7 @@ export function createResponsesHandler(options) {
109
109
  requestBodyTimeoutMs,
110
110
  responsesPayloadOptions,
111
111
  responsesFn,
112
+ responseFailures,
112
113
  streamHandshakeTimeoutMs,
113
114
  streamIdleTimeoutMs,
114
115
  upstreamTimeoutMs,
@@ -255,6 +256,7 @@ export function createResponsesHandler(options) {
255
256
  abort,
256
257
  onUpstreamStart: startUpstreamTimeout,
257
258
  releaseRequest: releaseUpstreamPayload,
259
+ responseFailures,
258
260
  streamIdleTimeoutMs,
259
261
  });
260
262
  if (result?.successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
@@ -8,8 +8,18 @@ import {
8
8
  sendUpstreamError,
9
9
  writeOrDrain,
10
10
  } from "./http-transport.mjs";
11
- import { openCopilotResponse } from "./copilot-responses-compat.mjs";
12
- import { rememberResponseHistory } from "./responses-request.mjs";
11
+ import { openCopilotResponse, selectCopilotResponseRetry } from "./copilot-responses-compat.mjs";
12
+ import {
13
+ dropMaterializedResponseHistory,
14
+ hasEncryptedResponseState,
15
+ rememberResponseHistory,
16
+ restoreMaterializedResponseHistoryForRetry,
17
+ } from "./responses-request.mjs";
18
+ import {
19
+ formatResponseFailureLog,
20
+ responseFailureDetails,
21
+ responseFailureText,
22
+ } from "./response-failures.mjs";
13
23
  import {
14
24
  compactionInputWithoutTrigger,
15
25
  parseResponsesCompactionResult,
@@ -30,9 +40,11 @@ import {
30
40
  } from "./stream-performance.mjs";
31
41
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
32
42
  import { recordResponsesUsage } from "./usage.mjs";
43
+ import { status } from "./status.mjs";
33
44
 
34
45
  const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
35
46
  const SSE_WRITE_BATCH_BYTES = 64 * 1024;
47
+ const STREAM_RETRY_PRELUDE_MAX_BYTES = 256 * 1024;
36
48
  const RESPONSES_TERMINAL_EVENT_TYPES = new Set([
37
49
  "response.completed",
38
50
  "response.incomplete",
@@ -124,7 +136,9 @@ function inspectResponseSseEvent(state, eventName, data) {
124
136
  );
125
137
  }
126
138
  state.sawTerminal = true;
127
- if (eventType === "response.failed" || eventType === "error") markStreamFailure();
139
+ if (eventType === "response.failed" || eventType === "error") {
140
+ state.failure = { event, eventType };
141
+ }
128
142
  if (eventType === "response.completed") {
129
143
  const response = event.response;
130
144
  state.completed = { response, event };
@@ -292,68 +306,182 @@ function storeCompletedResponse(reqContext, completed) {
292
306
  });
293
307
  }
294
308
 
295
- export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
296
- let opened;
309
+ function streamHeaders(resp) {
310
+ return {
311
+ ...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
312
+ "Cache-Control": "no-cache",
313
+ Connection: "keep-alive",
314
+ };
315
+ }
316
+
317
+ function selectStreamFailureRetry(reqContext, failure, usedRetryPolicies) {
318
+ if (!failure) return null;
297
319
  try {
298
- opened = await openCopilotResponse(reqContext, upstream, options);
299
- } finally {
300
- options.releaseRequest?.(opened?.reqContext);
301
- }
302
- const { resp, errorText } = opened;
303
- reqContext = opened.reqContext;
304
- if (errorText !== undefined) {
305
- sendUpstreamError(res, resp, errorText);
306
- return { successful: false, compacted: false, upstreamStatus: resp.status };
320
+ const restored = restoreMaterializedResponseHistoryForRetry(reqContext);
321
+ return {
322
+ retry: selectCopilotResponseRetry(
323
+ restored,
324
+ 400,
325
+ responseFailureText(failure.event, failure.eventType),
326
+ usedRetryPolicies,
327
+ ),
328
+ retrySkipped: null,
329
+ };
330
+ } catch {
331
+ return { retry: null, retrySkipped: "history_unavailable" };
307
332
  }
333
+ }
308
334
 
309
- if (reqContext.body.stream) {
335
+ function recordResponseFailure(options, reqContext, resp, failure, {
336
+ retry,
337
+ retrySkipped,
338
+ } = {}) {
339
+ if (!failure) return;
340
+ const details = responseFailureDetails(failure.event, failure.eventType, {
341
+ model: reqContext.body?.model,
342
+ headers: resp.headers,
343
+ retried: Boolean(retry),
344
+ retryPolicy: retry?.policyId,
345
+ retrySkipped,
346
+ });
347
+ options.responseFailures?.record?.(details);
348
+ console.warn(status("warn", formatResponseFailureLog(details)));
349
+ }
350
+
351
+ async function proxyStreamingResponses(opened, res, upstream, options) {
352
+ while (true) {
353
+ let {
354
+ resp,
355
+ errorText,
356
+ reqContext,
357
+ streamRetryEligible = false,
358
+ usedRetryPolicies = new Set(),
359
+ } = opened;
360
+ if (errorText !== undefined) {
361
+ sendUpstreamError(res, resp, errorText);
362
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
363
+ }
310
364
  await requireUpstreamEventStream(resp);
311
365
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
312
- res.writeHead(resp.status, {
313
- ...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
314
- "Cache-Control": "no-cache",
315
- Connection: "keep-alive",
316
- });
366
+ const headers = streamHeaders(resp);
367
+ let headersSent = false;
368
+ const writeHeaders = () => {
369
+ if (headersSent || res.headersSent) return;
370
+ headersSent = true;
371
+ res.writeHead(resp.status, headers);
372
+ };
317
373
  const reader = resp.body.getReader();
318
374
  const streamState = {
319
375
  completed: null,
376
+ failure: null,
320
377
  sawTerminal: false,
321
378
  sawOutput: false,
322
379
  toolArgumentGuard: new ToolArgumentDeltaGuard(),
323
380
  messageIds: new Map(),
324
381
  };
325
382
  const transformer = createResponseSseTransformer(streamState);
383
+ let holdPrelude = streamRetryEligible;
384
+ let retryDisabledReason = holdPrelude ? null : "request_not_eligible";
385
+ const prelude = [];
386
+ let preludeBytes = 0;
387
+ const flushPrelude = async () => {
388
+ if (!preludeBytes) return true;
389
+ writeHeaders();
390
+ const body = prelude.length === 1 ? prelude[0] : Buffer.concat(prelude, preludeBytes);
391
+ prelude.length = 0;
392
+ preludeBytes = 0;
393
+ return writeOrDrain(res, body);
394
+ };
395
+ const forwardFrame = async (frame) => {
396
+ if (!frame) return true;
397
+ if (!holdPrelude) {
398
+ writeHeaders();
399
+ return writeOrDrain(res, frame);
400
+ }
401
+ prelude.push(frame);
402
+ preludeBytes += frame.byteLength;
403
+ if (!streamState.sawOutput && preludeBytes <= STREAM_RETRY_PRELUDE_MAX_BYTES) return true;
404
+ retryDisabledReason = streamState.sawOutput ? "output_started" : "prelude_limit";
405
+ holdPrelude = false;
406
+ return flushPrelude();
407
+ };
408
+
326
409
  try {
327
410
  while (true) {
328
411
  const { done, value } = await reader.read();
329
412
  if (done) {
330
413
  if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
331
- storeCompletedResponse(reqContext, streamState.completed);
332
- res.end();
333
- return { successful: Boolean(streamState.completed), compacted: false };
414
+ break;
334
415
  }
335
416
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
336
- // Batch within this read, not across reads, and await drain before processing more.
337
417
  for (let offset = 0; offset < value.byteLength; offset += SSE_WRITE_BATCH_BYTES) {
338
418
  const frame = transformer.push(value.subarray(offset, offset + SSE_WRITE_BATCH_BYTES));
339
- if (frame && !await writeOrDrain(res, frame)) return;
419
+ if (!await forwardFrame(frame)) return { successful: false, compacted: false };
340
420
  if (streamState.sawTerminal) break;
341
421
  }
342
- if (streamState.sawTerminal) {
343
- storeCompletedResponse(reqContext, streamState.completed);
344
- res.end();
345
- return { successful: Boolean(streamState.completed), compacted: false };
346
- }
422
+ if (streamState.sawTerminal) break;
423
+ }
424
+
425
+ let retry = null;
426
+ if (streamState.failure && holdPrelude && !streamState.sawOutput) {
427
+ const selected = selectStreamFailureRetry(reqContext, streamState.failure, usedRetryPolicies);
428
+ retry = selected?.retry || null;
429
+ if (!retry) retryDisabledReason = selected?.retrySkipped || "not_applicable";
430
+ }
431
+ if (streamState.failure) {
432
+ recordResponseFailure(options, reqContext, resp, streamState.failure, {
433
+ retry,
434
+ retrySkipped: retry ? null : retryDisabledReason,
435
+ });
436
+ }
437
+ if (retry) {
438
+ console.warn(status("warn", retry.warning));
439
+ opened = await openCopilotResponse(retry.reqContext, upstream, {
440
+ ...options,
441
+ usedRetryPolicies: retry.usedPolicies,
442
+ });
443
+ opened.streamRetryEligible = hasEncryptedResponseState(opened.reqContext.body?.input);
444
+ dropMaterializedResponseHistory(opened.reqContext);
445
+ continue;
347
446
  }
348
- } catch (e) {
349
- logRequestFailure("Responses", e, options.abort);
350
- await endStreamWithError(res, e, options.abort);
447
+ if (streamState.failure) markStreamFailure();
448
+ if (holdPrelude && !await flushPrelude()) return { successful: false, compacted: false };
449
+ storeCompletedResponse(reqContext, streamState.completed);
450
+ if (!res.writableEnded) res.end();
451
+ return { successful: Boolean(streamState.completed), compacted: false };
452
+ } catch (error) {
453
+ logRequestFailure("Responses", error, options.abort);
454
+ writeHeaders();
455
+ await endStreamWithError(res, error, options.abort);
351
456
  return { successful: false, compacted: false };
352
457
  } finally {
353
458
  await reader.cancel().catch(() => {});
354
459
  reader.releaseLock();
355
460
  }
356
- } else {
461
+ }
462
+ }
463
+
464
+ export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
465
+ let opened;
466
+ try {
467
+ opened = await openCopilotResponse(reqContext, upstream, options);
468
+ opened.streamRetryEligible = opened.reqContext.body.stream === true
469
+ && hasEncryptedResponseState(opened.reqContext.body?.input);
470
+ } finally {
471
+ options.releaseRequest?.(opened?.reqContext);
472
+ }
473
+ reqContext = opened.reqContext;
474
+ if (reqContext.body.stream) {
475
+ return proxyStreamingResponses(opened, res, upstream, options);
476
+ }
477
+
478
+ while (true) {
479
+ const { resp, errorText, usedRetryPolicies = new Set() } = opened;
480
+ reqContext = opened.reqContext;
481
+ if (errorText !== undefined) {
482
+ sendUpstreamError(res, resp, errorText);
483
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
484
+ }
357
485
  const data = resp.ok
358
486
  ? await readBoundedResponseText(resp, {
359
487
  maxBytes: MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES,
@@ -385,6 +513,24 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
385
513
  }
386
514
  if (resp.ok) {
387
515
  const response = parseSuccessfulResponsesResult(data);
516
+ if (response.status === "failed") {
517
+ const failure = { event: { type: "response.failed", response }, eventType: "response.failed" };
518
+ const selected = selectStreamFailureRetry(reqContext, failure, usedRetryPolicies);
519
+ const retry = selected?.retry || null;
520
+ recordResponseFailure(options, reqContext, resp, failure, {
521
+ retry,
522
+ retrySkipped: retry ? null : selected?.retrySkipped || "not_applicable",
523
+ });
524
+ if (retry) {
525
+ console.warn(status("warn", retry.warning));
526
+ opened = await openCopilotResponse(retry.reqContext, upstream, {
527
+ ...options,
528
+ usedRetryPolicies: retry.usedPolicies,
529
+ });
530
+ dropMaterializedResponseHistory(opened.reqContext);
531
+ continue;
532
+ }
533
+ }
388
534
  rememberResponseHistory(reqContext, response);
389
535
  recordResponsesUsage({
390
536
  surface: reqContext.surface,
@@ -397,7 +543,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
397
543
  contentType: "application/json",
398
544
  }));
399
545
  res.end(data);
400
- return { successful: true, compacted: false };
546
+ return { successful: response.status !== "failed" && response.status !== "incomplete", compacted: false };
401
547
  }
402
548
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
403
549
  contentType: "application/json",
@@ -84,6 +84,7 @@ function isEncryptedContentPart(value) {
84
84
  const OMIT_ENCRYPTED_CONTENT = Symbol("omit-encrypted-content");
85
85
  const ENCRYPTED_HISTORY_REBASE = Symbol("encrypted-history-rebase");
86
86
  const RELEASE_HISTORY_INPUT = Symbol("release-history-input");
87
+ const RELEASED_MATERIALIZED_HISTORY = Symbol("released-materialized-history");
87
88
  const HISTORY_PRESSURE_ROOT = Symbol("history-pressure-root");
88
89
  const ENCRYPTED_TOOL_OUTPUT_MARKER = "[CCDX: encrypted tool output omitted because upstream could not decrypt it.]";
89
90
 
@@ -187,6 +188,10 @@ function responseValueHasOpaqueState(value) {
187
188
  return Object.values(value).some(responseValueHasOpaqueState);
188
189
  }
189
190
 
191
+ export function hasEncryptedResponseState(value) {
192
+ return responseValueHasOpaqueState(value);
193
+ }
194
+
190
195
  export function sanitizeEncryptedReasoningRequest(reqContext, { historicalOnly = false } = {}) {
191
196
  const state = { changed: false };
192
197
  let body = cloneJson(reqContext.body);
@@ -379,10 +384,28 @@ export function dropMaterializedResponseHistory(reqContext) {
379
384
  reqContext.body = { ...reqContext.body, input: releaseInput };
380
385
  reqContext.inputItems = releaseInput;
381
386
  delete reqContext[RELEASE_HISTORY_INPUT];
387
+ reqContext[RELEASED_MATERIALIZED_HISTORY] = true;
382
388
  reqContext.currentInputStart = 0;
383
389
  return true;
384
390
  }
385
391
 
392
+ export function restoreMaterializedResponseHistoryForRetry(reqContext, { assertActive } = {}) {
393
+ if (!reqContext?.[RELEASED_MATERIALIZED_HISTORY] || !reqContext.historyParentId) return reqContext;
394
+ const body = cloneJson(reqContext.body);
395
+ body.previous_response_id = reqContext.historyParentId;
396
+ body.input = cloneJson(reqContext.historyInputItems || reqContext.body.input);
397
+ let restored = prepareResponsesRequest(body, {
398
+ assertActive,
399
+ copilotBoundary: false,
400
+ mutate: true,
401
+ });
402
+ restored.surface = reqContext.surface;
403
+ if (reqContext.routePlan) {
404
+ restored = applyResponseHistoryRoutePlan(restored, reqContext.routePlan);
405
+ }
406
+ return restored;
407
+ }
408
+
386
409
  export function rememberResponseHistory(reqContext, responseJson) {
387
410
  if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
388
411
  const compacted = isSuccessfulCompactionResponse(responseJson);