opencodekit 0.18.23 → 0.18.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.
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "0.18.23";
23
+ var version = "0.18.25";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
Binary file
@@ -11,7 +11,7 @@
11
11
  "type-check": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@opencode-ai/plugin": "1.2.27"
14
+ "@opencode-ai/plugin": "1.3.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/node": "^25.3.0",
@@ -921,6 +921,7 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
921
921
  let fallbacksUsed = 0;
922
922
  let attempt = 0;
923
923
  let recoveryCyclesUsed = 0;
924
+ let attempted400Recovery = false;
924
925
 
925
926
  while (attempt <= RATE_LIMIT_CONFIG.maxRetries) {
926
927
  try {
@@ -1067,6 +1068,116 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
1067
1068
  );
1068
1069
  }
1069
1070
 
1071
+ // Handle 400 Bad Request with auto-recovery
1072
+ if (response.status === 400 && !attempted400Recovery) {
1073
+ let errorDetail = "Bad Request";
1074
+ try {
1075
+ const clonedResponse = response.clone();
1076
+ const errorBody = await clonedResponse.json();
1077
+ errorDetail =
1078
+ errorBody?.error?.message ||
1079
+ errorBody?.message ||
1080
+ "Bad Request";
1081
+ } catch {}
1082
+
1083
+ log(
1084
+ "warn",
1085
+ `[400-RECOVERY] Bad Request from Copilot API`,
1086
+ {
1087
+ model: currentModel,
1088
+ error_detail: errorDetail,
1089
+ attempt,
1090
+ },
1091
+ );
1092
+
1093
+ // Check for recoverable 400 causes
1094
+ const isThinkingBlockError =
1095
+ /thinking.?block|invalid.*signature|reasoning.*invalid/i.test(
1096
+ errorDetail,
1097
+ );
1098
+ const isIdError =
1099
+ /invalid.*\bid\b|item.*\bid\b|unknown.*\bid\b|malformed.*\bid\b/i.test(
1100
+ errorDetail,
1101
+ );
1102
+
1103
+ if (isThinkingBlockError || isIdError) {
1104
+ let bodyObj: any;
1105
+ try {
1106
+ bodyObj =
1107
+ typeof activeFinalInit.body === "string"
1108
+ ? JSON.parse(activeFinalInit.body)
1109
+ : activeFinalInit.body;
1110
+ } catch {
1111
+ // Can't parse body — not recoverable
1112
+ log(
1113
+ "warn",
1114
+ `[400-RECOVERY] Cannot parse request body, giving up`,
1115
+ );
1116
+ return response;
1117
+ }
1118
+
1119
+ // Cancel original response body only after confirming we can recover
1120
+ try {
1121
+ await response.body?.cancel();
1122
+ } catch {}
1123
+
1124
+ if (isThinkingBlockError && bodyObj?.messages) {
1125
+ // Strip ALL thinking/reasoning content aggressively
1126
+ bodyObj.messages = bodyObj.messages.map(
1127
+ (msg: any) => {
1128
+ if (msg.role !== "assistant") return msg;
1129
+ const {
1130
+ reasoning_text: _rt,
1131
+ reasoning_opaque: _ro,
1132
+ ...cleaned
1133
+ } = msg;
1134
+ if (Array.isArray(cleaned.content)) {
1135
+ cleaned.content = cleaned.content.filter(
1136
+ (part: any) => part.type !== "thinking",
1137
+ );
1138
+ if (cleaned.content.length === 0)
1139
+ cleaned.content = null;
1140
+ }
1141
+ return cleaned;
1142
+ },
1143
+ );
1144
+ delete bodyObj.thinking_budget;
1145
+ recovered = true;
1146
+ log(
1147
+ "info",
1148
+ `[400-RECOVERY] Stripped all thinking/reasoning content for retry`,
1149
+ );
1150
+ }
1151
+
1152
+ if (isIdError && bodyObj?.input) {
1153
+ bodyObj.input = sanitizeResponseInputIds(
1154
+ bodyObj.input,
1155
+ );
1156
+ recovered = true;
1157
+ log(
1158
+ "info",
1159
+ `[400-RECOVERY] Re-sanitized Responses API IDs for retry`,
1160
+ );
1161
+ }
1162
+
1163
+ if (recovered) {
1164
+ attempted400Recovery = true;
1165
+ activeFinalInit = {
1166
+ ...activeFinalInit,
1167
+ body: JSON.stringify(bodyObj),
1168
+ };
1169
+ attempt++;
1170
+ continue;
1171
+ }
1172
+ }
1173
+
1174
+ // Not recoverable — log detail and return original response
1175
+ log(
1176
+ "warn",
1177
+ `[400-RECOVERY] Non-recoverable 400: ${errorDetail}`,
1178
+ );
1179
+ }
1180
+
1070
1181
  // Response transformation is handled by the custom SDK at
1071
1182
  // .opencode/plugin/sdk/copilot/
1072
1183
  return response;
@@ -43,15 +43,29 @@ interface HookDeps {
43
43
  function extractErrorMessage(value: unknown, maxLen = 200): string {
44
44
  if (!value) return "Unknown error";
45
45
  if (typeof value === "string") return value.slice(0, maxLen);
46
- if (value instanceof Error) return (value.message || value.name || "Error").slice(0, maxLen);
46
+ if (value instanceof Error)
47
+ return (value.message || value.name || "Error").slice(0, maxLen);
47
48
  if (typeof value === "object" && value !== null) {
48
49
  const obj = value as Record<string, unknown>;
50
+
51
+ // Handle OpenCode error structure: { name, data: { message, statusCode } }
52
+ if (typeof obj.data === "object" && obj.data !== null) {
53
+ const data = obj.data as Record<string, unknown>;
54
+ if (typeof data.message === "string") {
55
+ const prefix = typeof obj.name === "string" ? `${obj.name}: ` : "";
56
+ const status =
57
+ typeof data.statusCode === "number" ? ` (${data.statusCode})` : "";
58
+ return `${prefix}${data.message}${status}`.slice(0, maxLen);
59
+ }
60
+ }
61
+
49
62
  // Common error shapes: { message }, { error }, { error: { message } }
50
63
  if (typeof obj.message === "string") return obj.message.slice(0, maxLen);
51
64
  if (typeof obj.error === "string") return obj.error.slice(0, maxLen);
52
65
  if (typeof obj.error === "object" && obj.error !== null) {
53
66
  const inner = obj.error as Record<string, unknown>;
54
- if (typeof inner.message === "string") return inner.message.slice(0, maxLen);
67
+ if (typeof inner.message === "string")
68
+ return inner.message.slice(0, maxLen);
55
69
  }
56
70
  // Last resort: JSON stringify with truncation
57
71
  try {
@@ -123,7 +137,22 @@ export function createHooks(deps: HookDeps) {
123
137
  // --- Session error: classify and guide ---
124
138
  if (event.type === "session.error") {
125
139
  const props = event.properties as Record<string, unknown> | undefined;
126
- const errorMsg = extractErrorMessage(props?.error ?? props?.message ?? "Unknown error");
140
+ const errorObj = props?.error ?? props?.message ?? "Unknown error";
141
+ const errorMsg = extractErrorMessage(errorObj);
142
+
143
+ // Extract status code from error object for classification
144
+ const rawCode =
145
+ typeof errorObj === "object" && errorObj !== null
146
+ ? ((errorObj as Record<string, unknown>).data as Record<string, unknown>
147
+ )?.statusCode ??
148
+ (errorObj as Record<string, unknown>).statusCode
149
+ : undefined;
150
+ const statusCode =
151
+ typeof rawCode === "number"
152
+ ? rawCode
153
+ : typeof rawCode === "string"
154
+ ? Number(rawCode) || undefined
155
+ : undefined;
127
156
 
128
157
  // Log full error for debugging
129
158
  await log(`Session error: ${errorMsg}`, "warn");
@@ -136,13 +165,39 @@ export function createHooks(deps: HookDeps) {
136
165
  ) {
137
166
  guidance = "Context too large — use /compact or start a new session";
138
167
  } else if (
139
- /rate.?limit|429|too many requests/i.test(errorMsg)
168
+ /rate.?limit|too many requests/i.test(errorMsg) ||
169
+ statusCode === 429
140
170
  ) {
141
171
  guidance = "Rate limited — wait a moment and retry";
142
172
  } else if (
143
- /unauthorized|401|403|auth/i.test(errorMsg)
173
+ /unauthorized|auth/i.test(errorMsg) ||
174
+ statusCode === 401 ||
175
+ statusCode === 403
144
176
  ) {
145
177
  guidance = "Auth error — check API key or token";
178
+ } else if (
179
+ statusCode === 400 ||
180
+ /bad request|invalid.*request/i.test(errorMsg)
181
+ ) {
182
+ // Sub-classify 400 errors for more specific guidance
183
+ if (
184
+ /thinking.?block|invalid.*signature|reasoning/i.test(errorMsg)
185
+ ) {
186
+ guidance =
187
+ "Thinking block error — start a new session to reset";
188
+ } else if (
189
+ /context.*(too|exceed|length|large|limit)|too.?long|max.?length|content.?length/i.test(errorMsg)
190
+ ) {
191
+ guidance =
192
+ "Request too large — use /compact to reduce context";
193
+ } else if (
194
+ /invalid.*\bid\b|item.*\bid\b|unknown.*\bid\b|malformed.*\bid\b/i.test(errorMsg)
195
+ ) {
196
+ guidance = "API format error — start a new session";
197
+ } else {
198
+ guidance =
199
+ "Bad request — try starting a new session or using /compact";
200
+ }
146
201
  } else if (
147
202
  /timeout|ETIMEDOUT|ECONNRESET|network|fetch failed/i.test(errorMsg)
148
203
  ) {
@@ -152,14 +207,20 @@ export function createHooks(deps: HookDeps) {
152
207
  ) {
153
208
  guidance = "API format error — try starting a new session";
154
209
  } else if (
155
- /500|502|503|504|internal server|service unavailable/i.test(errorMsg)
210
+ statusCode === 500 ||
211
+ statusCode === 502 ||
212
+ statusCode === 503 ||
213
+ statusCode === 504 ||
214
+ /internal server|service unavailable/i.test(errorMsg)
156
215
  ) {
157
216
  guidance = "Server error — retry in a few seconds";
158
217
  } else {
159
- guidance = "Unexpected error — save work with observation tool if needed";
218
+ guidance =
219
+ "Unexpected error — save work with observation tool if needed";
160
220
  }
161
221
 
162
- const short = errorMsg.length > 80 ? `${errorMsg.slice(0, 80)}…` : errorMsg;
222
+ const short =
223
+ errorMsg.length > 100 ? `${errorMsg.slice(0, 100)}…` : errorMsg;
163
224
  await showToast("Session Error", `${guidance} (${short})`, "warning");
164
225
  }
165
226
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.18.23",
3
+ "version": "0.18.25",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",