opencode-copilot-account-switcher 0.11.0 → 0.12.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.
@@ -1,19 +1,7 @@
1
1
  import { appendFileSync } from "node:fs";
2
- const RETRYABLE_MESSAGES = [
3
- "load failed",
4
- "failed to fetch",
5
- "network request failed",
6
- "unable to connect",
7
- "econnreset",
8
- "etimedout",
9
- "socket hang up",
10
- "unknown certificate",
11
- "self signed certificate",
12
- "unable to verify the first certificate",
13
- "self-signed certificate in certificate chain",
14
- ];
15
- const AI_ERROR_MARKER = Symbol.for("vercel.ai.error");
16
- const API_CALL_ERROR_MARKER = Symbol.for("vercel.ai.error.AI_APICallError");
2
+ import { getSharedErrorMessage, noopSharedRetryNotifier, notifySharedRetryEvent, runSharedFailOpenBoundary, runSharedRetryScheduler, } from "./retry/shared-engine.js";
3
+ import { createNetworkRetryEngine } from "./network-retry-engine.js";
4
+ import { createCopilotRetryPolicy, isRetryableCopilotTransportError, } from "./retry/copilot-policy.js";
17
5
  const INTERNAL_SESSION_HEADER = "x-opencode-session-id";
18
6
  const INTERNAL_DEBUG_LINK_HEADER = "x-opencode-debug-link-id";
19
7
  export const INTERNAL_SESSION_CONTEXT_KEY = "__opencodeInternalSessionID";
@@ -44,7 +32,7 @@ function isAbortError(error) {
44
32
  return error instanceof Error && error.name === "AbortError";
45
33
  }
46
34
  function getErrorMessage(error) {
47
- return String(error instanceof Error ? error.message : error).toLowerCase();
35
+ return getSharedErrorMessage(error);
48
36
  }
49
37
  function isInputIdTooLongErrorBody(payload) {
50
38
  if (!payload || typeof payload !== "object")
@@ -422,21 +410,6 @@ function buildRetryInit(init, payload) {
422
410
  body: JSON.stringify(payload),
423
411
  };
424
412
  }
425
- const noopNotifier = {
426
- started: async () => { },
427
- progress: async () => { },
428
- repairWarning: async () => { },
429
- completed: async () => { },
430
- stopped: async () => { },
431
- };
432
- async function notify(notifier, event, remaining) {
433
- try {
434
- await notifier[event]({ remaining });
435
- }
436
- catch (error) {
437
- console.warn(`[copilot-network-retry] notifier ${event} failed`, error);
438
- }
439
- }
440
413
  function stripInternalSessionHeaderFromRequest(request) {
441
414
  if (!(request instanceof Request))
442
415
  return request;
@@ -705,52 +678,21 @@ async function repairSessionParts(sessionID, itemIds, ctx) {
705
678
  }
706
679
  return patchedAll;
707
680
  }
708
- async function maybeRetryConnectionMismatchItemIds(request, init, response, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
709
- if (response.ok) {
710
- return {
711
- response,
712
- retried: false,
713
- nextInit: init,
714
- nextPayload: requestPayload,
715
- retryState: undefined,
716
- };
717
- }
681
+ async function maybeRetryConnectionMismatchItemIds(request, init, currentResponse, decision, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
718
682
  const removableIds = collectInputItemIds(requestPayload);
719
683
  if (!requestPayload || removableIds.length === 0) {
720
684
  return {
721
- response,
685
+ response: currentResponse,
722
686
  retried: false,
723
687
  nextInit: init,
724
688
  nextPayload: requestPayload,
725
689
  retryState: undefined,
726
690
  };
727
691
  }
728
- const responseText = await response
729
- .clone()
730
- .text()
731
- .catch(() => "");
692
+ const responseText = decision.responseText;
732
693
  if (!responseText) {
733
694
  return {
734
- response,
735
- retried: false,
736
- nextInit: init,
737
- nextPayload: requestPayload,
738
- retryState: undefined,
739
- };
740
- }
741
- let matched = isConnectionMismatchInputIdMessage(responseText);
742
- if (!matched) {
743
- try {
744
- const bodyPayload = JSON.parse(responseText);
745
- matched = isConnectionMismatchInputIdErrorBody(bodyPayload);
746
- }
747
- catch {
748
- matched = false;
749
- }
750
- }
751
- if (!matched) {
752
- return {
753
- response,
695
+ response: currentResponse,
754
696
  retried: false,
755
697
  nextInit: init,
756
698
  nextPayload: requestPayload,
@@ -760,13 +702,23 @@ async function maybeRetryConnectionMismatchItemIds(request, init, response, base
760
702
  const remainingBefore = countInputIdCandidates(requestPayload);
761
703
  const notifiedStarted = startedNotified || remainingBefore > 0;
762
704
  let repairFailed = false;
763
- if (sessionID) {
764
- repairFailed = !(await repairSessionParts(sessionID, removableIds, ctx).catch(() => false));
705
+ if (decision.shouldAttemptSessionRepair && sessionID) {
706
+ const repairResult = await runSharedFailOpenBoundary({
707
+ action: () => repairSessionParts(sessionID, removableIds, ctx),
708
+ isFailOpenError: () => true,
709
+ onFailOpen: (error) => {
710
+ debugLog("input-id retry session bulk repair failed-open", {
711
+ sessionID,
712
+ error: String(error instanceof Error ? error.message : error),
713
+ });
714
+ },
715
+ });
716
+ repairFailed = !(repairResult.ok ? repairResult.value : false);
765
717
  }
766
718
  const sanitized = stripAllInputIds(requestPayload);
767
719
  if (sanitized === requestPayload) {
768
720
  return {
769
- response,
721
+ response: currentResponse,
770
722
  retried: false,
771
723
  nextInit: init,
772
724
  nextPayload: requestPayload,
@@ -803,20 +755,11 @@ async function maybeRetryConnectionMismatchItemIds(request, init, response, base
803
755
  });
804
756
  return { response: retried, retried: true, nextInit, nextPayload: sanitized, retryState };
805
757
  }
806
- async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
807
- if (response.ok) {
808
- return {
809
- response,
810
- retried: false,
811
- nextInit: init,
812
- nextPayload: requestPayload,
813
- retryState: undefined,
814
- };
815
- }
758
+ async function maybeRetryInputIdTooLong(request, init, currentResponse, decision, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
816
759
  if (!requestPayload || !hasLongInputIds(requestPayload)) {
817
760
  debugLog("skip input-id retry: request has no long ids");
818
761
  return {
819
- response,
762
+ response: currentResponse,
820
763
  retried: false,
821
764
  nextInit: init,
822
765
  nextPayload: requestPayload,
@@ -824,38 +767,27 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requ
824
767
  };
825
768
  }
826
769
  debugLog("input-id retry candidate", {
827
- status: response.status,
828
- contentType: response.headers.get("content-type") ?? undefined,
770
+ serverReportedIndex: decision.serverReportedIndex,
771
+ reportedLength: decision.reportedLength,
829
772
  });
830
- const responseText = await response
831
- .clone()
832
- .text()
833
- .catch(() => "");
773
+ const responseText = decision.responseText;
834
774
  if (!responseText) {
835
775
  debugLog("skip input-id retry: empty response body");
836
776
  return {
837
- response,
777
+ response: currentResponse,
838
778
  retried: false,
839
779
  nextInit: init,
840
780
  nextPayload: requestPayload,
841
781
  retryState: undefined,
842
782
  };
843
783
  }
844
- let parsed = parseInputIdTooLongDetails(responseText);
845
- let matched = parsed.matched;
846
- if (!matched) {
847
- try {
848
- const bodyPayload = JSON.parse(responseText);
849
- const error = bodyPayload.error;
850
- parsed = parseInputIdTooLongDetails(String(error?.message ?? ""));
851
- matched = parsed.matched || isInputIdTooLongErrorBody(bodyPayload);
852
- }
853
- catch {
854
- matched = false;
855
- }
856
- }
784
+ const parsed = {
785
+ matched: true,
786
+ serverReportedIndex: decision.serverReportedIndex,
787
+ reportedLength: decision.reportedLength,
788
+ };
857
789
  debugLog("input-id retry detection", {
858
- matched,
790
+ matched: true,
859
791
  serverReportedIndex: parsed.serverReportedIndex,
860
792
  reportedLength: parsed.reportedLength,
861
793
  bodyPreview: responseText.slice(0, 200),
@@ -864,15 +796,6 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requ
864
796
  serverReportedIndex: parsed.serverReportedIndex,
865
797
  reportedLength: parsed.reportedLength,
866
798
  });
867
- if (!matched) {
868
- return {
869
- response,
870
- retried: false,
871
- nextInit: init,
872
- nextPayload: requestPayload,
873
- retryState: undefined,
874
- };
875
- }
876
799
  const payloadCandidates = getPayloadCandidates(requestPayload);
877
800
  const targetSelection = getTargetedLongInputIdSelection(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
878
801
  debugLog("input-id retry payload candidates", {
@@ -900,7 +823,7 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requ
900
823
  reportedLengthMatched: targetSelection.reportedLengthMatched,
901
824
  });
902
825
  return {
903
- response,
826
+ response: currentResponse,
904
827
  retried: false,
905
828
  nextInit: init,
906
829
  nextPayload: requestPayload,
@@ -918,14 +841,24 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requ
918
841
  }
919
842
  const notifiedStarted = startedNotified || remainingBefore > 0;
920
843
  let repairFailed = false;
921
- if (sessionID && failingId) {
922
- repairFailed = !(await repairSessionPart(sessionID, failingId, ctx).catch(() => false));
844
+ if (decision.shouldAttemptSessionRepair && sessionID && failingId) {
845
+ const repairResult = await runSharedFailOpenBoundary({
846
+ action: () => repairSessionPart(sessionID, failingId, ctx),
847
+ isFailOpenError: () => true,
848
+ onFailOpen: (error) => {
849
+ debugLog("input-id retry session repair failed-open", {
850
+ sessionID,
851
+ error: String(error instanceof Error ? error.message : error),
852
+ });
853
+ },
854
+ });
855
+ repairFailed = !(repairResult.ok ? repairResult.value : false);
923
856
  }
924
857
  const sanitized = stripTargetedLongInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
925
858
  if (sanitized === requestPayload) {
926
859
  debugLog("skip input-id retry: sanitize made no changes");
927
860
  return {
928
- response,
861
+ response: currentResponse,
929
862
  retried: false,
930
863
  nextInit: init,
931
864
  nextPayload: requestPayload,
@@ -961,49 +894,6 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requ
961
894
  });
962
895
  return { response: retried, retried: true, nextInit, nextPayload: sanitized, retryState };
963
896
  }
964
- function getRequestUrl(request) {
965
- return request instanceof Request ? request.url : request instanceof URL ? request.href : String(request);
966
- }
967
- function buildRetryableApiCallMessage(group, detail) {
968
- return `Copilot retryable error [${group}]: ${detail}`;
969
- }
970
- function toRetryableApiCallError(error, request, options) {
971
- const base = error instanceof Error ? error : new Error(String(error));
972
- const wrapped = new Error(buildRetryableApiCallMessage(options?.group ?? "transport", base.message));
973
- wrapped.name = "AI_APICallError";
974
- wrapped.url = getRequestUrl(request);
975
- wrapped.requestBodyValues = options?.requestBodyValues ?? {};
976
- wrapped.statusCode = options?.statusCode;
977
- wrapped.responseHeaders = options?.responseHeaders instanceof Headers
978
- ? Object.fromEntries(options.responseHeaders.entries())
979
- : options?.responseHeaders;
980
- wrapped.responseBody = options?.responseBody;
981
- wrapped.isRetryable = true;
982
- wrapped.cause = error;
983
- wrapped[AI_ERROR_MARKER] = true;
984
- wrapped[API_CALL_ERROR_MARKER] = true;
985
- return wrapped;
986
- }
987
- function isRetryableApiCallError(error) {
988
- return Boolean(error
989
- && typeof error === "object"
990
- && error[AI_ERROR_MARKER] === true
991
- && error[API_CALL_ERROR_MARKER] === true);
992
- }
993
- function isRetryableCopilotStatus(status) {
994
- return status === 499;
995
- }
996
- function isCopilotUrl(request) {
997
- const raw = request instanceof Request ? request.url : request instanceof URL ? request.href : String(request);
998
- try {
999
- const url = new URL(raw);
1000
- const isCopilotHost = url.hostname === "api.githubcopilot.com" || url.hostname.startsWith("copilot-api.");
1001
- return isCopilotHost;
1002
- }
1003
- catch {
1004
- return false;
1005
- }
1006
- }
1007
897
  async function getInputIdRetryErrorDetails(response) {
1008
898
  if (response.ok)
1009
899
  return undefined;
@@ -1055,7 +945,7 @@ async function parseJsonRequestPayload(request, init) {
1055
945
  return undefined;
1056
946
  }
1057
947
  }
1058
- function withStreamDebugLogs(response, request) {
948
+ function withStreamDebugLogs(response, request, policy) {
1059
949
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
1060
950
  if (!contentType.includes("text/event-stream") || !response.body)
1061
951
  return response;
@@ -1077,8 +967,14 @@ function withStreamDebugLogs(response, request) {
1077
967
  }
1078
968
  catch (error) {
1079
969
  const message = getErrorMessage(error);
1080
- const isSseReadTimeout = message.includes("sse read timed out");
1081
- const retryable = RETRYABLE_MESSAGES.some((part) => message.includes(part));
970
+ const normalized = policy.normalizeStreamError({
971
+ error,
972
+ request,
973
+ statusCode: response.status,
974
+ responseHeaders: response.headers,
975
+ });
976
+ const isSseReadTimeout = normalized === error && message.includes("sse read timed out");
977
+ const retryable = normalized !== error;
1082
978
  if (isDebugEnabled()) {
1083
979
  debugLog("sse stream read error", {
1084
980
  url: rawUrl,
@@ -1087,15 +983,7 @@ function withStreamDebugLogs(response, request) {
1087
983
  bypassedTimeoutWrap: isSseReadTimeout,
1088
984
  });
1089
985
  }
1090
- controller.error(isSseReadTimeout
1091
- ? error
1092
- : retryable
1093
- ? toRetryableApiCallError(error, request, {
1094
- group: "stream",
1095
- statusCode: response.status,
1096
- responseHeaders: response.headers,
1097
- })
1098
- : error);
986
+ controller.error(isSseReadTimeout ? error : normalized);
1099
987
  }
1100
988
  };
1101
989
  void pump();
@@ -1108,10 +996,7 @@ function withStreamDebugLogs(response, request) {
1108
996
  });
1109
997
  }
1110
998
  export function isRetryableCopilotFetchError(error) {
1111
- if (!error || isAbortError(error))
1112
- return false;
1113
- const message = getErrorMessage(error);
1114
- return RETRYABLE_MESSAGES.some((part) => message.includes(part));
999
+ return isRetryableCopilotTransportError(error);
1115
1000
  }
1116
1001
  function isRetryableCopilotJsonParseError(error) {
1117
1002
  if (!error || isAbortError(error))
@@ -1121,9 +1006,166 @@ function isRetryableCopilotJsonParseError(error) {
1121
1006
  const hasAiJsonParseSignature = name === "AI_JSONParseError" || message.includes("ai_jsonparseerror");
1122
1007
  return hasAiJsonParseSignature && message.includes("json parsing failed") && message.includes("text:");
1123
1008
  }
1009
+ function toRequestUrl(request) {
1010
+ return request instanceof Request ? request.url : request instanceof URL ? request.href : String(request);
1011
+ }
1012
+ async function runCopilotRepairLoop(input) {
1013
+ const state = {
1014
+ currentResponse: input.response,
1015
+ currentInit: input.effectiveInit,
1016
+ currentPayload: input.currentPayload,
1017
+ attempts: 0,
1018
+ startedNotified: false,
1019
+ finishedNotified: false,
1020
+ repairWarningNotified: false,
1021
+ };
1022
+ const handleRetryResult = async (result) => {
1023
+ state.currentResponse = result.response;
1024
+ state.currentInit = result.nextInit;
1025
+ state.currentPayload = result.nextPayload;
1026
+ if (!result.retryState) {
1027
+ return {
1028
+ handled: false,
1029
+ stop: false,
1030
+ shouldContinue: false,
1031
+ };
1032
+ }
1033
+ if (!state.startedNotified && result.retryState.notifiedStarted) {
1034
+ state.startedNotified = true;
1035
+ await notifySharedRetryEvent(input.notifier, "started", result.retryState.remainingLongIdCandidatesBefore);
1036
+ }
1037
+ if (result.retryState.repairFailed && !state.repairWarningNotified) {
1038
+ await notifySharedRetryEvent(input.notifier, "repairWarning", result.retryState.remainingLongIdCandidatesBefore);
1039
+ state.repairWarningNotified = true;
1040
+ }
1041
+ const currentError = await getInputIdRetryErrorDetails(state.currentResponse);
1042
+ let stopReason = result.retryState.stopReason;
1043
+ const madeProgress = result.retryState.remainingLongIdCandidatesAfter < result.retryState.remainingLongIdCandidatesBefore;
1044
+ if (!stopReason && result.retryState.remainingLongIdCandidatesAfter >= result.retryState.remainingLongIdCandidatesBefore) {
1045
+ stopReason = "remaining-candidates-not-reduced";
1046
+ }
1047
+ if (!stopReason
1048
+ && currentError
1049
+ && result.retryState.remainingLongIdCandidatesAfter > 0
1050
+ && result.retryState.previousServerReportedIndex === currentError.serverReportedIndex
1051
+ && result.retryState.previousReportedLength === currentError.reportedLength) {
1052
+ stopReason = "same-server-item-persists";
1053
+ }
1054
+ if (!stopReason && currentError && result.retryState.remainingLongIdCandidatesAfter === 0) {
1055
+ stopReason = "local-candidates-exhausted";
1056
+ }
1057
+ if ((currentError || stopReason) && stopReason !== "evidence-insufficient") {
1058
+ debugLog("input-id retry progress", {
1059
+ attempt: state.attempts + 1,
1060
+ previousServerReportedIndex: result.retryState.previousServerReportedIndex,
1061
+ currentServerReportedIndex: currentError?.serverReportedIndex,
1062
+ serverIndexChanged: result.retryState.previousServerReportedIndex !== currentError?.serverReportedIndex,
1063
+ previousErrorMessagePreview: result.retryState.previousErrorMessagePreview,
1064
+ currentErrorMessagePreview: currentError?.errorMessagePreview,
1065
+ remainingLongIdCandidatesBefore: result.retryState.remainingLongIdCandidatesBefore,
1066
+ remainingLongIdCandidatesAfter: result.retryState.remainingLongIdCandidatesAfter,
1067
+ stopReason,
1068
+ });
1069
+ }
1070
+ if (stopReason === "local-candidates-exhausted") {
1071
+ logCleanupStopped("local-candidates-exhausted", {
1072
+ attempt: state.attempts + 1,
1073
+ previousServerReportedIndex: result.retryState.previousServerReportedIndex,
1074
+ currentServerReportedIndex: currentError?.serverReportedIndex,
1075
+ });
1076
+ }
1077
+ if (stopReason) {
1078
+ await notifySharedRetryEvent(input.notifier, "stopped", result.retryState.remainingLongIdCandidatesAfter);
1079
+ state.finishedNotified = true;
1080
+ return {
1081
+ handled: true,
1082
+ stop: true,
1083
+ shouldContinue: false,
1084
+ };
1085
+ }
1086
+ if (result.retried && madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0) {
1087
+ await notifySharedRetryEvent(input.notifier, "progress", result.retryState.remainingLongIdCandidatesAfter);
1088
+ }
1089
+ if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && state.currentResponse.ok) {
1090
+ await notifySharedRetryEvent(input.notifier, "completed", 0);
1091
+ state.finishedNotified = true;
1092
+ }
1093
+ if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && !state.currentResponse.ok) {
1094
+ await notifySharedRetryEvent(input.notifier, "stopped", 0);
1095
+ state.finishedNotified = true;
1096
+ }
1097
+ if (!result.retried) {
1098
+ if (state.startedNotified && !state.finishedNotified) {
1099
+ await notifySharedRetryEvent(input.notifier, "stopped", result.retryState.remainingLongIdCandidatesAfter);
1100
+ state.finishedNotified = true;
1101
+ }
1102
+ return {
1103
+ handled: true,
1104
+ stop: true,
1105
+ shouldContinue: false,
1106
+ };
1107
+ }
1108
+ return {
1109
+ handled: true,
1110
+ stop: false,
1111
+ shouldContinue: madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0,
1112
+ };
1113
+ };
1114
+ await runSharedRetryScheduler({
1115
+ initialShouldContinue: countInputIdCandidates(state.currentPayload) > 0,
1116
+ runIteration: async ({ attempts: scheduledAttempts }) => {
1117
+ state.attempts = scheduledAttempts;
1118
+ const decision = await input.policy.decideResponseRepair({
1119
+ request: input.safeRequest,
1120
+ response: state.currentResponse,
1121
+ requestPayload: state.currentPayload,
1122
+ sessionID: input.sessionID,
1123
+ });
1124
+ if (decision.kind === "skip") {
1125
+ if (state.startedNotified && !state.finishedNotified) {
1126
+ await notifySharedRetryEvent(input.notifier, "stopped", countInputIdCandidates(state.currentPayload));
1127
+ state.finishedNotified = true;
1128
+ }
1129
+ return {
1130
+ handled: false,
1131
+ stop: true,
1132
+ shouldContinue: false,
1133
+ };
1134
+ }
1135
+ const result = decision.kind === "connection-mismatch"
1136
+ ? await maybeRetryConnectionMismatchItemIds(input.safeRequest, state.currentInit, state.currentResponse, decision, input.baseFetch, state.currentPayload, input.options, input.sessionID, state.startedNotified)
1137
+ : await maybeRetryInputIdTooLong(input.safeRequest, state.currentInit, state.currentResponse, decision, input.baseFetch, state.currentPayload, input.options, input.sessionID, state.startedNotified);
1138
+ const handled = await handleRetryResult(result);
1139
+ if (handled.stop)
1140
+ return handled;
1141
+ if (!handled.handled) {
1142
+ if (state.startedNotified && !state.finishedNotified) {
1143
+ await notifySharedRetryEvent(input.notifier, "stopped", countInputIdCandidates(state.currentPayload));
1144
+ state.finishedNotified = true;
1145
+ }
1146
+ return {
1147
+ handled: false,
1148
+ stop: true,
1149
+ shouldContinue: false,
1150
+ };
1151
+ }
1152
+ return handled;
1153
+ },
1154
+ });
1155
+ return {
1156
+ response: state.currentResponse,
1157
+ payload: state.currentPayload,
1158
+ };
1159
+ }
1124
1160
  export function createCopilotRetryingFetch(baseFetch, options) {
1125
- const notifier = options?.notifier ?? noopNotifier;
1126
- return async function retryingFetch(request, init) {
1161
+ const notifier = options?.notifier ?? noopSharedRetryNotifier;
1162
+ const policy = createCopilotRetryPolicy({
1163
+ extraRetryableClassifier: isRetryableCopilotJsonParseError,
1164
+ });
1165
+ const retryEngine = createNetworkRetryEngine({
1166
+ policy,
1167
+ });
1168
+ return retryEngine(async (request, init) => {
1127
1169
  const sessionID = getHeader(request, init, INTERNAL_SESSION_HEADER);
1128
1170
  const headersBeforeWrapper = toHeaderRecord(init?.headers) ?? (request instanceof Request ? toHeaderRecord(request.headers) : undefined);
1129
1171
  debugLog("fetch headers before wrapper", {
@@ -1145,157 +1187,38 @@ export function createCopilotRetryingFetch(baseFetch, options) {
1145
1187
  removedInternalHeaders: strippedHeaders.removed,
1146
1188
  isRetry: false,
1147
1189
  });
1190
+ const isCopilotRequest = policy.matchesRequest(safeRequest);
1148
1191
  debugLog("fetch start", {
1149
1192
  url: safeRequest instanceof Request ? safeRequest.url : safeRequest instanceof URL ? safeRequest.href : String(safeRequest),
1150
- isCopilot: isCopilotUrl(safeRequest),
1193
+ isCopilot: isCopilotRequest,
1151
1194
  });
1152
1195
  debugLog("fetch headers before network", {
1153
1196
  headers: toHeaderRecord(effectiveInit?.headers) ?? (safeRequest instanceof Request ? toHeaderRecord(safeRequest.headers) : undefined),
1154
1197
  isRetry: false,
1155
1198
  });
1156
- let currentPayload = await parseJsonRequestPayload(safeRequest, effectiveInit);
1199
+ const currentPayload = await parseJsonRequestPayload(safeRequest, effectiveInit);
1157
1200
  try {
1158
1201
  const response = await baseFetch(safeRequest, effectiveInit);
1159
1202
  debugLog("fetch resolved", {
1160
1203
  status: response.status,
1161
1204
  contentType: response.headers.get("content-type") ?? undefined,
1162
1205
  });
1163
- if (isCopilotUrl(safeRequest) && isRetryableCopilotStatus(response.status)) {
1164
- const responseBody = await response.clone().text().catch(() => "");
1165
- throw toRetryableApiCallError(new Error(responseBody || `status code ${response.status}`), safeRequest, {
1166
- group: "status",
1167
- requestBodyValues: currentPayload,
1168
- statusCode: response.status,
1169
- responseHeaders: response.headers,
1170
- responseBody: responseBody || undefined,
1206
+ if (isCopilotRequest && policy.shouldRunResponseRepair(safeRequest)) {
1207
+ const repaired = await runCopilotRepairLoop({
1208
+ safeRequest,
1209
+ effectiveInit,
1210
+ response,
1211
+ baseFetch,
1212
+ policy,
1213
+ options,
1214
+ notifier,
1215
+ sessionID,
1216
+ currentPayload,
1171
1217
  });
1218
+ return withStreamDebugLogs(repaired.response, safeRequest, policy);
1172
1219
  }
1173
- if (isCopilotUrl(safeRequest)) {
1174
- let currentResponse = response;
1175
- let currentInit = effectiveInit;
1176
- let attempts = 0;
1177
- let shouldContinueInputIdRepair = countInputIdCandidates(currentPayload) > 0;
1178
- let startedNotified = false;
1179
- let finishedNotified = false;
1180
- let repairWarningNotified = false;
1181
- const handleRetryResult = async (result) => {
1182
- currentResponse = result.response;
1183
- currentInit = result.nextInit;
1184
- currentPayload = result.nextPayload;
1185
- if (!result.retryState) {
1186
- return {
1187
- handled: false,
1188
- stop: false,
1189
- shouldContinue: false,
1190
- };
1191
- }
1192
- if (!startedNotified && result.retryState.notifiedStarted) {
1193
- startedNotified = true;
1194
- await notify(notifier, "started", result.retryState.remainingLongIdCandidatesBefore);
1195
- }
1196
- if (result.retryState.repairFailed && !repairWarningNotified) {
1197
- await notify(notifier, "repairWarning", result.retryState.remainingLongIdCandidatesBefore);
1198
- repairWarningNotified = true;
1199
- }
1200
- const currentError = await getInputIdRetryErrorDetails(currentResponse);
1201
- let stopReason = result.retryState.stopReason;
1202
- const madeProgress = result.retryState.remainingLongIdCandidatesAfter < result.retryState.remainingLongIdCandidatesBefore;
1203
- if (!stopReason && result.retryState.remainingLongIdCandidatesAfter >= result.retryState.remainingLongIdCandidatesBefore) {
1204
- stopReason = "remaining-candidates-not-reduced";
1205
- }
1206
- if (!stopReason &&
1207
- currentError &&
1208
- result.retryState.remainingLongIdCandidatesAfter > 0 &&
1209
- result.retryState.previousServerReportedIndex === currentError.serverReportedIndex &&
1210
- result.retryState.previousReportedLength === currentError.reportedLength) {
1211
- stopReason = "same-server-item-persists";
1212
- }
1213
- if (!stopReason && currentError && result.retryState.remainingLongIdCandidatesAfter === 0) {
1214
- stopReason = "local-candidates-exhausted";
1215
- }
1216
- if ((currentError || stopReason) && stopReason !== "evidence-insufficient") {
1217
- debugLog("input-id retry progress", {
1218
- attempt: attempts + 1,
1219
- previousServerReportedIndex: result.retryState.previousServerReportedIndex,
1220
- currentServerReportedIndex: currentError?.serverReportedIndex,
1221
- serverIndexChanged: result.retryState.previousServerReportedIndex !== currentError?.serverReportedIndex,
1222
- previousErrorMessagePreview: result.retryState.previousErrorMessagePreview,
1223
- currentErrorMessagePreview: currentError?.errorMessagePreview,
1224
- remainingLongIdCandidatesBefore: result.retryState.remainingLongIdCandidatesBefore,
1225
- remainingLongIdCandidatesAfter: result.retryState.remainingLongIdCandidatesAfter,
1226
- stopReason,
1227
- });
1228
- }
1229
- if (stopReason === "local-candidates-exhausted") {
1230
- logCleanupStopped("local-candidates-exhausted", {
1231
- attempt: attempts + 1,
1232
- previousServerReportedIndex: result.retryState.previousServerReportedIndex,
1233
- currentServerReportedIndex: currentError?.serverReportedIndex,
1234
- });
1235
- }
1236
- if (stopReason) {
1237
- await notify(notifier, "stopped", result.retryState.remainingLongIdCandidatesAfter);
1238
- finishedNotified = true;
1239
- return {
1240
- handled: true,
1241
- stop: true,
1242
- shouldContinue: false,
1243
- };
1244
- }
1245
- if (result.retried && madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0) {
1246
- await notify(notifier, "progress", result.retryState.remainingLongIdCandidatesAfter);
1247
- }
1248
- if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && currentResponse.ok) {
1249
- await notify(notifier, "completed", 0);
1250
- finishedNotified = true;
1251
- }
1252
- if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && !currentResponse.ok) {
1253
- await notify(notifier, "stopped", 0);
1254
- finishedNotified = true;
1255
- }
1256
- if (!result.retried) {
1257
- if (startedNotified && !finishedNotified) {
1258
- await notify(notifier, "stopped", result.retryState.remainingLongIdCandidatesAfter);
1259
- finishedNotified = true;
1260
- }
1261
- return {
1262
- handled: true,
1263
- stop: true,
1264
- shouldContinue: false,
1265
- };
1266
- }
1267
- return {
1268
- handled: true,
1269
- stop: false,
1270
- shouldContinue: madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0,
1271
- };
1272
- };
1273
- while (shouldContinueInputIdRepair) {
1274
- shouldContinueInputIdRepair = false;
1275
- const connectionMismatchResult = await maybeRetryConnectionMismatchItemIds(safeRequest, currentInit, currentResponse, baseFetch, currentPayload, options, sessionID, startedNotified);
1276
- const handledConnectionMismatch = await handleRetryResult(connectionMismatchResult);
1277
- if (handledConnectionMismatch.stop)
1278
- break;
1279
- if (handledConnectionMismatch.handled) {
1280
- attempts += 1;
1281
- shouldContinueInputIdRepair = handledConnectionMismatch.shouldContinue;
1282
- continue;
1283
- }
1284
- const result = await maybeRetryInputIdTooLong(safeRequest, currentInit, currentResponse, baseFetch, currentPayload, options, sessionID, startedNotified);
1285
- const handled = await handleRetryResult(result);
1286
- if (handled.stop)
1287
- break;
1288
- if (!handled.handled) {
1289
- if (startedNotified && !finishedNotified) {
1290
- await notify(notifier, "stopped", countInputIdCandidates(currentPayload));
1291
- finishedNotified = true;
1292
- }
1293
- break;
1294
- }
1295
- attempts += 1;
1296
- shouldContinueInputIdRepair = handled.shouldContinue;
1297
- }
1298
- return withStreamDebugLogs(currentResponse, safeRequest);
1220
+ if (isCopilotRequest) {
1221
+ return withStreamDebugLogs(response, safeRequest, policy);
1299
1222
  }
1300
1223
  return response;
1301
1224
  }
@@ -1307,16 +1230,7 @@ export function createCopilotRetryingFetch(baseFetch, options) {
1307
1230
  retryableByMessage,
1308
1231
  retryableCopilotJsonParse,
1309
1232
  });
1310
- if (!isCopilotUrl(safeRequest) || (!retryableByMessage && !retryableCopilotJsonParse)) {
1311
- throw error;
1312
- }
1313
- if (isRetryableApiCallError(error)) {
1314
- throw error;
1315
- }
1316
- throw toRetryableApiCallError(error, safeRequest, {
1317
- group: "transport",
1318
- requestBodyValues: currentPayload,
1319
- });
1233
+ throw error;
1320
1234
  }
1321
- };
1235
+ });
1322
1236
  }