opencode-copilot-account-switcher 0.2.6 → 0.2.8
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/README.md +107 -116
- package/dist/copilot-network-retry.d.ts +39 -4
- package/dist/copilot-network-retry.js +376 -32
- package/dist/loop-safety-plugin.d.ts +1 -1
- package/dist/loop-safety-plugin.js +7 -1
- package/dist/plugin-hooks.d.ts +27 -3
- package/dist/plugin-hooks.js +13 -3
- package/dist/plugin.js +8 -1
- package/dist/store.js +2 -2
- package/dist/ui/menu.d.ts +29 -1
- package/dist/ui/menu.js +86 -25
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ const RETRYABLE_MESSAGES = [
|
|
|
13
13
|
"unable to verify the first certificate",
|
|
14
14
|
"self-signed certificate in certificate chain",
|
|
15
15
|
];
|
|
16
|
+
const INPUT_ID_REPAIR_HARD_LIMIT = 64;
|
|
17
|
+
const INTERNAL_SESSION_HEADER = "x-opencode-session-id";
|
|
16
18
|
const defaultDebugLogFile = (() => {
|
|
17
19
|
const tmp = process.env.TEMP || process.env.TMP || "/tmp";
|
|
18
20
|
return `${tmp}/opencode-copilot-retry-debug.log`;
|
|
@@ -47,11 +49,42 @@ function isInputIdTooLongErrorBody(payload) {
|
|
|
47
49
|
return false;
|
|
48
50
|
const error = payload.error;
|
|
49
51
|
const message = String(error?.message ?? "").toLowerCase();
|
|
50
|
-
return message.includes("
|
|
52
|
+
return message.includes("string too long") && (message.includes("input id") || message.includes(".id'"));
|
|
51
53
|
}
|
|
52
54
|
function isInputIdTooLongMessage(text) {
|
|
53
55
|
const message = text.toLowerCase();
|
|
54
|
-
return message.includes("
|
|
56
|
+
return message.includes("string too long") && (message.includes("input id") || message.includes(".id'"));
|
|
57
|
+
}
|
|
58
|
+
function parseInputIdTooLongDetails(text) {
|
|
59
|
+
const matched = isInputIdTooLongMessage(text);
|
|
60
|
+
if (!matched)
|
|
61
|
+
return { matched };
|
|
62
|
+
const index = text.match(/input\[(\d+)\]\.id/i);
|
|
63
|
+
const length = text.match(/got a string with length\s+(\d+)/i) ?? text.match(/length\s+(\d+)/i);
|
|
64
|
+
return {
|
|
65
|
+
matched,
|
|
66
|
+
serverReportedIndex: index ? Number(index[1]) : undefined,
|
|
67
|
+
reportedLength: length ? Number(length[1]) : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildIdPreview(id) {
|
|
71
|
+
return `${id.slice(0, 12)}...`;
|
|
72
|
+
}
|
|
73
|
+
function buildMessagePreview(message) {
|
|
74
|
+
return message.slice(0, 160);
|
|
75
|
+
}
|
|
76
|
+
function getPayloadCandidates(payload) {
|
|
77
|
+
const input = payload.input;
|
|
78
|
+
if (!Array.isArray(input))
|
|
79
|
+
return [];
|
|
80
|
+
return input.flatMap((item, payloadIndex) => {
|
|
81
|
+
const id = item?.id;
|
|
82
|
+
if (typeof id !== "string" || id.length <= 64)
|
|
83
|
+
return [];
|
|
84
|
+
const content = item?.content;
|
|
85
|
+
const itemKind = Array.isArray(content) && typeof content[0]?.type === "string" ? String(content[0].type) : "unknown";
|
|
86
|
+
return [{ payloadIndex, idLength: id.length, itemKind, idPreview: buildIdPreview(id) }];
|
|
87
|
+
});
|
|
55
88
|
}
|
|
56
89
|
function hasLongInputIds(payload) {
|
|
57
90
|
const input = payload.input;
|
|
@@ -59,22 +92,62 @@ function hasLongInputIds(payload) {
|
|
|
59
92
|
return false;
|
|
60
93
|
return input.some((item) => typeof item?.id === "string" && (item.id?.length ?? 0) > 64);
|
|
61
94
|
}
|
|
62
|
-
function
|
|
95
|
+
function countLongInputIdCandidates(payload) {
|
|
96
|
+
const input = payload?.input;
|
|
97
|
+
if (!Array.isArray(input))
|
|
98
|
+
return 0;
|
|
99
|
+
return input.filter((item) => typeof item?.id === "string" && (item.id?.length ?? 0) > 64)
|
|
100
|
+
.length;
|
|
101
|
+
}
|
|
102
|
+
function collectLongInputIdCandidates(payload) {
|
|
63
103
|
const input = payload.input;
|
|
64
104
|
if (!Array.isArray(input))
|
|
105
|
+
return [];
|
|
106
|
+
return input.flatMap((item, payloadIndex) => {
|
|
107
|
+
const id = item?.id;
|
|
108
|
+
if (typeof id !== "string" || id.length <= 64)
|
|
109
|
+
return [];
|
|
110
|
+
return [{ item: item, payloadIndex, idLength: id.length }];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function pickCandidateByServerIndexHint(candidates, serverReportedIndex) {
|
|
114
|
+
const hintedPayloadIndex = serverReportedIndex - 1;
|
|
115
|
+
return candidates
|
|
116
|
+
.filter((candidate) => candidate.payloadIndex >= hintedPayloadIndex)
|
|
117
|
+
.sort((left, right) => left.payloadIndex - right.payloadIndex)[0];
|
|
118
|
+
}
|
|
119
|
+
function getTargetedLongInputId(payload, serverReportedIndex, reportedLength) {
|
|
120
|
+
const matches = collectLongInputIdCandidates(payload);
|
|
121
|
+
if (matches.length === 0)
|
|
122
|
+
return undefined;
|
|
123
|
+
const lengthMatches = reportedLength
|
|
124
|
+
? matches.filter((item) => item.idLength === reportedLength)
|
|
125
|
+
: matches;
|
|
126
|
+
if (lengthMatches.length === 1)
|
|
127
|
+
return lengthMatches[0].item;
|
|
128
|
+
if (matches.length === 1)
|
|
129
|
+
return matches[0].item;
|
|
130
|
+
const narrowedCandidates = lengthMatches.length > 0 ? lengthMatches : matches;
|
|
131
|
+
if (typeof serverReportedIndex === "number") {
|
|
132
|
+
return pickCandidateByServerIndexHint(narrowedCandidates, serverReportedIndex)?.item;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
function stripTargetedLongInputId(payload, serverReportedIndex, reportedLength) {
|
|
137
|
+
const input = payload.input;
|
|
138
|
+
if (!Array.isArray(input))
|
|
139
|
+
return payload;
|
|
140
|
+
const target = getTargetedLongInputId(payload, serverReportedIndex, reportedLength);
|
|
141
|
+
if (!target)
|
|
65
142
|
return payload;
|
|
66
143
|
let changed = false;
|
|
67
144
|
const nextInput = input.map((item) => {
|
|
68
|
-
if (
|
|
145
|
+
if (item !== target)
|
|
69
146
|
return item;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
delete clone.id;
|
|
75
|
-
return clone;
|
|
76
|
-
}
|
|
77
|
-
return item;
|
|
147
|
+
changed = true;
|
|
148
|
+
const clone = { ...item };
|
|
149
|
+
delete clone.id;
|
|
150
|
+
return clone;
|
|
78
151
|
});
|
|
79
152
|
if (!changed)
|
|
80
153
|
return payload;
|
|
@@ -98,6 +171,7 @@ function parseJsonBody(init) {
|
|
|
98
171
|
}
|
|
99
172
|
function buildRetryInit(init, payload) {
|
|
100
173
|
const headers = new Headers(init?.headers);
|
|
174
|
+
headers.delete(INTERNAL_SESSION_HEADER);
|
|
101
175
|
if (!headers.has("content-type")) {
|
|
102
176
|
headers.set("content-type", "application/json");
|
|
103
177
|
}
|
|
@@ -107,13 +181,149 @@ function buildRetryInit(init, payload) {
|
|
|
107
181
|
body: JSON.stringify(payload),
|
|
108
182
|
};
|
|
109
183
|
}
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
return
|
|
184
|
+
function stripInternalSessionHeaderFromRequest(request) {
|
|
185
|
+
if (!(request instanceof Request))
|
|
186
|
+
return request;
|
|
187
|
+
if (!request.headers.has(INTERNAL_SESSION_HEADER))
|
|
188
|
+
return request;
|
|
189
|
+
const headers = new Headers(request.headers);
|
|
190
|
+
headers.delete(INTERNAL_SESSION_HEADER);
|
|
191
|
+
return new Request(request, { headers });
|
|
192
|
+
}
|
|
193
|
+
function getHeader(request, init, name) {
|
|
194
|
+
const initHeaders = new Headers(init?.headers);
|
|
195
|
+
const initValue = initHeaders.get(name);
|
|
196
|
+
if (initValue)
|
|
197
|
+
return initValue;
|
|
198
|
+
if (request instanceof Request)
|
|
199
|
+
return request.headers.get(name) ?? undefined;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function getTargetedInputId(payload, serverReportedIndex, reportedLength) {
|
|
203
|
+
const target = getTargetedLongInputId(payload, serverReportedIndex, reportedLength);
|
|
204
|
+
const id = target?.id;
|
|
205
|
+
if (typeof id !== "string")
|
|
206
|
+
return undefined;
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
function stripOpenAIItemId(part) {
|
|
210
|
+
const metadata = part.metadata;
|
|
211
|
+
if (!metadata || typeof metadata !== "object")
|
|
212
|
+
return part;
|
|
213
|
+
const openai = metadata.openai;
|
|
214
|
+
if (!openai || typeof openai !== "object")
|
|
215
|
+
return part;
|
|
216
|
+
if (!Object.hasOwn(openai, "itemId"))
|
|
217
|
+
return part;
|
|
218
|
+
const nextOpenai = { ...openai };
|
|
219
|
+
delete nextOpenai.itemId;
|
|
220
|
+
return {
|
|
221
|
+
...part,
|
|
222
|
+
metadata: {
|
|
223
|
+
...metadata,
|
|
224
|
+
openai: nextOpenai,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function repairSessionPart(sessionID, failingId, ctx) {
|
|
229
|
+
const messages = await ctx?.client?.session?.messages?.({
|
|
230
|
+
path: { id: sessionID },
|
|
231
|
+
});
|
|
232
|
+
const matches = (messages?.data ?? []).flatMap((message) => {
|
|
233
|
+
if (message.info?.role !== "assistant")
|
|
234
|
+
return [];
|
|
235
|
+
return (message.parts ?? []).flatMap((part) => {
|
|
236
|
+
const itemId = part.metadata?.openai?.itemId;
|
|
237
|
+
if (itemId !== failingId || typeof message.info?.id !== "string" || typeof part.id !== "string")
|
|
238
|
+
return [];
|
|
239
|
+
return [{ messageID: message.info.id, partID: part.id, partType: String(part.type ?? "unknown") }];
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
debugLog("input-id retry session candidates", {
|
|
243
|
+
sessionID,
|
|
244
|
+
count: matches.length,
|
|
245
|
+
candidates: matches,
|
|
246
|
+
});
|
|
247
|
+
if (matches.length !== 1)
|
|
248
|
+
return false;
|
|
249
|
+
const match = matches[0];
|
|
250
|
+
debugLog("input-id retry session match", match);
|
|
251
|
+
const latest = await ctx?.client?.session?.message?.({
|
|
252
|
+
path: {
|
|
253
|
+
id: sessionID,
|
|
254
|
+
messageID: match.messageID,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const part = latest?.data?.parts?.find((item) => item.id === match.partID);
|
|
258
|
+
if (!part)
|
|
259
|
+
return false;
|
|
260
|
+
const body = stripOpenAIItemId(part);
|
|
261
|
+
const url = new URL(`/session/${sessionID}/message/${match.messageID}/part/${match.partID}`, ctx?.serverUrl);
|
|
262
|
+
if (ctx?.directory)
|
|
263
|
+
url.searchParams.set("directory", ctx.directory);
|
|
264
|
+
const init = {
|
|
265
|
+
method: "PATCH",
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
};
|
|
271
|
+
if (ctx?.patchPart) {
|
|
272
|
+
try {
|
|
273
|
+
await ctx.patchPart({ url: url.href, init });
|
|
274
|
+
debugLog("input-id retry session repair", {
|
|
275
|
+
partID: match.partID,
|
|
276
|
+
messageID: match.messageID,
|
|
277
|
+
sessionID,
|
|
278
|
+
});
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
debugLog("input-id retry session repair failed", {
|
|
283
|
+
partID: match.partID,
|
|
284
|
+
messageID: match.messageID,
|
|
285
|
+
sessionID,
|
|
286
|
+
error: String(error instanceof Error ? error.message : error),
|
|
287
|
+
});
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const response = await fetch(url, init);
|
|
293
|
+
debugLog("input-id retry session repair", {
|
|
294
|
+
partID: match.partID,
|
|
295
|
+
messageID: match.messageID,
|
|
296
|
+
sessionID,
|
|
297
|
+
ok: response.ok,
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
debugLog("input-id retry session repair failed", {
|
|
301
|
+
partID: match.partID,
|
|
302
|
+
messageID: match.messageID,
|
|
303
|
+
sessionID,
|
|
304
|
+
status: response.status,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return response.ok;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
debugLog("input-id retry session repair failed", {
|
|
311
|
+
partID: match.partID,
|
|
312
|
+
messageID: match.messageID,
|
|
313
|
+
sessionID,
|
|
314
|
+
error: String(error instanceof Error ? error.message : error),
|
|
315
|
+
});
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx, sessionID) {
|
|
320
|
+
if (response.status !== 400) {
|
|
321
|
+
return { response, retried: false, nextInit: init, retryState: undefined };
|
|
322
|
+
}
|
|
113
323
|
const requestPayload = parseJsonBody(init);
|
|
114
324
|
if (!requestPayload || !hasLongInputIds(requestPayload)) {
|
|
115
325
|
debugLog("skip input-id retry: request has no long ids");
|
|
116
|
-
return response;
|
|
326
|
+
return { response, retried: false, nextInit: init, retryState: undefined };
|
|
117
327
|
}
|
|
118
328
|
debugLog("input-id retry candidate", {
|
|
119
329
|
status: response.status,
|
|
@@ -125,13 +335,16 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch) {
|
|
|
125
335
|
.catch(() => "");
|
|
126
336
|
if (!responseText) {
|
|
127
337
|
debugLog("skip input-id retry: empty response body");
|
|
128
|
-
return response;
|
|
338
|
+
return { response, retried: false, nextInit: init, retryState: undefined };
|
|
129
339
|
}
|
|
130
|
-
let
|
|
340
|
+
let parsed = parseInputIdTooLongDetails(responseText);
|
|
341
|
+
let matched = parsed.matched;
|
|
131
342
|
if (!matched) {
|
|
132
343
|
try {
|
|
133
344
|
const bodyPayload = JSON.parse(responseText);
|
|
134
|
-
|
|
345
|
+
const error = bodyPayload.error;
|
|
346
|
+
parsed = parseInputIdTooLongDetails(String(error?.message ?? ""));
|
|
347
|
+
matched = parsed.matched || isInputIdTooLongErrorBody(bodyPayload);
|
|
135
348
|
}
|
|
136
349
|
catch {
|
|
137
350
|
matched = false;
|
|
@@ -139,25 +352,73 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch) {
|
|
|
139
352
|
}
|
|
140
353
|
debugLog("input-id retry detection", {
|
|
141
354
|
matched,
|
|
355
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
356
|
+
reportedLength: parsed.reportedLength,
|
|
142
357
|
bodyPreview: responseText.slice(0, 200),
|
|
143
358
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
359
|
+
debugLog("input-id retry parsed", {
|
|
360
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
361
|
+
reportedLength: parsed.reportedLength,
|
|
362
|
+
});
|
|
363
|
+
if (!matched) {
|
|
364
|
+
return { response, retried: false, nextInit: init, retryState: undefined };
|
|
365
|
+
}
|
|
366
|
+
if (parsed.serverReportedIndex === undefined) {
|
|
367
|
+
debugLog("skip input-id retry: missing server input index", {
|
|
368
|
+
reportedLength: parsed.reportedLength,
|
|
369
|
+
});
|
|
370
|
+
return { response, retried: false, nextInit: init, retryState: undefined };
|
|
371
|
+
}
|
|
372
|
+
const payloadCandidates = getPayloadCandidates(requestPayload);
|
|
373
|
+
debugLog("input-id retry payload candidates", {
|
|
374
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
375
|
+
candidates: payloadCandidates,
|
|
376
|
+
});
|
|
377
|
+
const failingId = getTargetedInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
378
|
+
const targetedPayload = payloadCandidates.find((item) => item.idLength === parsed.reportedLength) ?? payloadCandidates[0];
|
|
379
|
+
if (targetedPayload) {
|
|
380
|
+
debugLog("input-id retry payload target", {
|
|
381
|
+
targetedPayloadIndex: targetedPayload.payloadIndex,
|
|
382
|
+
itemKind: targetedPayload.itemKind,
|
|
383
|
+
idLength: targetedPayload.idLength,
|
|
384
|
+
idPreview: targetedPayload.idPreview,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (sessionID && failingId) {
|
|
388
|
+
await repairSessionPart(sessionID, failingId, ctx).catch(() => false);
|
|
389
|
+
}
|
|
390
|
+
const sanitized = stripTargetedLongInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
147
391
|
if (sanitized === requestPayload) {
|
|
148
392
|
debugLog("skip input-id retry: sanitize made no changes");
|
|
149
|
-
return
|
|
393
|
+
return {
|
|
394
|
+
response,
|
|
395
|
+
retried: false,
|
|
396
|
+
nextInit: init,
|
|
397
|
+
retryState: {
|
|
398
|
+
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
399
|
+
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
400
|
+
remainingLongIdCandidatesBefore: countLongInputIdCandidates(requestPayload),
|
|
401
|
+
remainingLongIdCandidatesAfter: countLongInputIdCandidates(requestPayload),
|
|
402
|
+
},
|
|
403
|
+
};
|
|
150
404
|
}
|
|
151
405
|
debugLog("input-id retry triggered", {
|
|
152
406
|
removedLongIds: true,
|
|
153
407
|
hadPreviousResponseId: typeof requestPayload.previous_response_id === "string",
|
|
154
408
|
});
|
|
155
|
-
const
|
|
409
|
+
const nextInit = buildRetryInit(init, sanitized);
|
|
410
|
+
const retried = await baseFetch(request, nextInit);
|
|
411
|
+
const retryState = {
|
|
412
|
+
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
413
|
+
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
414
|
+
remainingLongIdCandidatesBefore: countLongInputIdCandidates(requestPayload),
|
|
415
|
+
remainingLongIdCandidatesAfter: countLongInputIdCandidates(parseJsonBody(nextInit)),
|
|
416
|
+
};
|
|
156
417
|
debugLog("input-id retry response", {
|
|
157
418
|
status: retried.status,
|
|
158
419
|
contentType: retried.headers.get("content-type") ?? undefined,
|
|
159
420
|
});
|
|
160
|
-
return retried;
|
|
421
|
+
return { response: retried, retried: true, nextInit, retryState };
|
|
161
422
|
}
|
|
162
423
|
function toRetryableSystemError(error) {
|
|
163
424
|
const base = error instanceof Error ? error : new Error(String(error));
|
|
@@ -179,6 +440,37 @@ function isCopilotUrl(request) {
|
|
|
179
440
|
return false;
|
|
180
441
|
}
|
|
181
442
|
}
|
|
443
|
+
async function getInputIdRetryErrorDetails(response) {
|
|
444
|
+
if (response.status !== 400)
|
|
445
|
+
return undefined;
|
|
446
|
+
const responseText = await response
|
|
447
|
+
.clone()
|
|
448
|
+
.text()
|
|
449
|
+
.catch(() => "");
|
|
450
|
+
if (!responseText)
|
|
451
|
+
return undefined;
|
|
452
|
+
let parsed = parseInputIdTooLongDetails(responseText);
|
|
453
|
+
let matched = parsed.matched;
|
|
454
|
+
let message = responseText;
|
|
455
|
+
if (!matched) {
|
|
456
|
+
try {
|
|
457
|
+
const bodyPayload = JSON.parse(responseText);
|
|
458
|
+
const error = bodyPayload.error;
|
|
459
|
+
message = String(error?.message ?? "");
|
|
460
|
+
parsed = parseInputIdTooLongDetails(message);
|
|
461
|
+
matched = parsed.matched || isInputIdTooLongErrorBody(bodyPayload);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
matched = false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!matched)
|
|
468
|
+
return undefined;
|
|
469
|
+
return {
|
|
470
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
471
|
+
errorMessagePreview: buildMessagePreview(message),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
182
474
|
function withStreamDebugLogs(response, request) {
|
|
183
475
|
if (!isDebugEnabled())
|
|
184
476
|
return response;
|
|
@@ -229,19 +521,71 @@ export function isRetryableCopilotFetchError(error) {
|
|
|
229
521
|
export function createCopilotRetryingFetch(baseFetch, options) {
|
|
230
522
|
void options;
|
|
231
523
|
return async function retryingFetch(request, init) {
|
|
524
|
+
const sessionID = getHeader(request, init, INTERNAL_SESSION_HEADER);
|
|
525
|
+
const safeRequest = stripInternalSessionHeaderFromRequest(request);
|
|
526
|
+
const initHeaders = new Headers(init?.headers);
|
|
527
|
+
initHeaders.delete(INTERNAL_SESSION_HEADER);
|
|
528
|
+
const effectiveInit = init
|
|
529
|
+
? {
|
|
530
|
+
...init,
|
|
531
|
+
headers: initHeaders,
|
|
532
|
+
}
|
|
533
|
+
: undefined;
|
|
232
534
|
debugLog("fetch start", {
|
|
233
|
-
url:
|
|
234
|
-
isCopilot: isCopilotUrl(
|
|
535
|
+
url: safeRequest instanceof Request ? safeRequest.url : safeRequest instanceof URL ? safeRequest.href : String(safeRequest),
|
|
536
|
+
isCopilot: isCopilotUrl(safeRequest),
|
|
235
537
|
});
|
|
236
538
|
try {
|
|
237
|
-
const response = await baseFetch(
|
|
539
|
+
const response = await baseFetch(safeRequest, effectiveInit);
|
|
238
540
|
debugLog("fetch resolved", {
|
|
239
541
|
status: response.status,
|
|
240
542
|
contentType: response.headers.get("content-type") ?? undefined,
|
|
241
543
|
});
|
|
242
|
-
if (isCopilotUrl(
|
|
243
|
-
|
|
244
|
-
|
|
544
|
+
if (isCopilotUrl(safeRequest)) {
|
|
545
|
+
let currentResponse = response;
|
|
546
|
+
let currentInit = effectiveInit;
|
|
547
|
+
let attempts = 0;
|
|
548
|
+
while (attempts < INPUT_ID_REPAIR_HARD_LIMIT) {
|
|
549
|
+
const remainingCandidates = countLongInputIdCandidates(parseJsonBody(currentInit));
|
|
550
|
+
if (remainingCandidates === 0)
|
|
551
|
+
break;
|
|
552
|
+
const result = await maybeRetryInputIdTooLong(safeRequest, currentInit, currentResponse, baseFetch, options, sessionID);
|
|
553
|
+
currentResponse = result.response;
|
|
554
|
+
currentInit = result.nextInit;
|
|
555
|
+
if (result.retryState) {
|
|
556
|
+
const currentError = await getInputIdRetryErrorDetails(currentResponse);
|
|
557
|
+
let stopReason;
|
|
558
|
+
if (result.retryState.remainingLongIdCandidatesAfter >= result.retryState.remainingLongIdCandidatesBefore) {
|
|
559
|
+
stopReason = "remaining-candidates-not-reduced";
|
|
560
|
+
}
|
|
561
|
+
else if (currentError) {
|
|
562
|
+
const serverIndexChanged = result.retryState.previousServerReportedIndex !== currentError.serverReportedIndex;
|
|
563
|
+
const errorMessageChanged = result.retryState.previousErrorMessagePreview !== currentError.errorMessagePreview;
|
|
564
|
+
if (!serverIndexChanged && !errorMessageChanged) {
|
|
565
|
+
stopReason = "server-error-unchanged";
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (currentError || stopReason) {
|
|
569
|
+
debugLog("input-id retry progress", {
|
|
570
|
+
attempt: attempts + 1,
|
|
571
|
+
previousServerReportedIndex: result.retryState.previousServerReportedIndex,
|
|
572
|
+
currentServerReportedIndex: currentError?.serverReportedIndex,
|
|
573
|
+
serverIndexChanged: result.retryState.previousServerReportedIndex !== currentError?.serverReportedIndex,
|
|
574
|
+
previousErrorMessagePreview: result.retryState.previousErrorMessagePreview,
|
|
575
|
+
currentErrorMessagePreview: currentError?.errorMessagePreview,
|
|
576
|
+
remainingLongIdCandidatesBefore: result.retryState.remainingLongIdCandidatesBefore,
|
|
577
|
+
remainingLongIdCandidatesAfter: result.retryState.remainingLongIdCandidatesAfter,
|
|
578
|
+
stopReason,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (stopReason)
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
if (!result.retried)
|
|
585
|
+
break;
|
|
586
|
+
attempts += 1;
|
|
587
|
+
}
|
|
588
|
+
return withStreamDebugLogs(currentResponse, safeRequest);
|
|
245
589
|
}
|
|
246
590
|
return response;
|
|
247
591
|
}
|
|
@@ -250,7 +594,7 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
250
594
|
message: getErrorMessage(error),
|
|
251
595
|
retryableByMessage: isRetryableCopilotFetchError(error),
|
|
252
596
|
});
|
|
253
|
-
if (!isCopilotUrl(
|
|
597
|
+
if (!isCopilotUrl(safeRequest) || !isRetryableCopilotFetchError(error)) {
|
|
254
598
|
throw error;
|
|
255
599
|
}
|
|
256
600
|
throw toRetryableSystemError(error);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Hooks } from "@opencode-ai/plugin";
|
|
2
2
|
import { type StoreFile } from "./store.js";
|
|
3
|
-
export declare const LOOP_SAFETY_POLICY = "Guided Loop Safety Policy\n- Continue working on any remaining non-blocked task before stopping to report or wait for more instructions.\n- If you are not fully blocked, do not stop just because you feel ready to pause; finish the work that can still be done safely.\n- MANDATORY: When the question tool is available and permitted in the current session, ALL user-facing reports MUST be delivered through the question tool.\n- MANDATORY: Treat progress updates, status reports, summaries, explanations, completion notes, and next-step choices as user-facing reports.\n- MANDATORY: In this workflow, the question tool is both the question channel and the report channel for user-visible communication.\n- MANDATORY: Do not reinterpret \"keep working unless blocked\" as permission to send direct assistant text. If you need to report, report through the question tool.\n- MANDATORY: Do not reinterpret \"don't ask unnecessary questions\" as permission to skip the question tool. Use fewer question batches, not a different reporting channel.\n- MANDATORY: If the user says a report was sent the wrong way, first check whether you used the wrong channel before analyzing verbosity, timing, or wording.\n- MANDATORY: When reflecting on mistakes, check report-channel violations before any secondary critique.\n- MANDATORY: Do not turn a pure report into a preference question, permission question, or route-selection question just to satisfy the question tool requirement.\n- MANDATORY: When no user decision is needed, use the question tool as a report container with the smallest sensible option set.\n- NO EXCEPTIONS: Do not switch to direct assistant text just because the report is long, detailed, complex, or contains multiple related items.\n- The question tool is considered available and permitted when it appears in the active tool list and the current session has not denied its use.\n- Direct assistant text is allowed only when the question tool is unavailable, denied, or absent from the current session.\n- If a report would otherwise be sent as direct text, convert it into one question batch or a short sequence of question batches instead.\n- IMPORTANT: A report-mode violation usually means the wrong tool was used, not that the report was too long.\n- IMPORTANT: Do not reinterpret a tool-usage violation as a verbosity problem.\n- Long reports are allowed. Use pagination or sequential question batches when needed; do not fall back to direct assistant text.\n- A single question tool call may contain multiple well-grouped questions. Prefer that over multiple small interruptions when it keeps the report clear.\n- Group related items into explicit batches such as current progress, key findings, decisions, and next-step choices.\n- Present the highest-priority information first and defer secondary details to later question batches when needed.\n- Even when no explicit decision is required, use brief question-tool status updates instead of direct assistant text whenever the tool is available.\n- Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.\n- When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.\n- When the user says \"stop\", do not send assistant text to acknowledge the stop. Use the question tool only if more user-visible communication is still required by policy.\n- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.\n- Materially improves the result means clearly beneficial cases such as parallel analysis of independent areas; it does not include routine local searches, small file reads, or straightforward edits.\n- If task or subagent delegation is used, keep the number minimal and explain the reason briefly through the question tool when available.";
|
|
3
|
+
export declare const LOOP_SAFETY_POLICY = "Guided Loop Safety Policy\n- Continue working on any remaining non-blocked task before stopping to report or wait for more instructions.\n- If you are not fully blocked, do not stop just because you feel ready to pause; finish the work that can still be done safely.\n- MANDATORY: When the question tool is available and permitted in the current session, ALL user-facing reports MUST be delivered through the question tool.\n- MANDATORY: Treat progress updates, status reports, summaries, explanations, completion notes, and next-step choices as user-facing reports.\n- MANDATORY: In this workflow, the question tool is both the question channel and the report channel for user-visible communication.\n- MANDATORY: Do not reinterpret \"keep working unless blocked\" as permission to send direct assistant text. If you need to report, report through the question tool.\n- MANDATORY: Do not reinterpret \"don't ask unnecessary questions\" as permission to skip the question tool. Use fewer question batches, not a different reporting channel.\n- MANDATORY: If the user says a report was sent the wrong way, first check whether you used the wrong channel before analyzing verbosity, timing, or wording.\n- MANDATORY: When reflecting on report-delivery or tool-usage mistakes, first check report-channel violations before any secondary critique.\n- MANDATORY: Do not reinterpret those report-channel violations as a problem with the report policy itself.\n- MANDATORY: Do not turn a pure report into a preference question, permission question, or route-selection question just to satisfy the question tool requirement.\n- MANDATORY: When no user decision is needed, use the question tool as a report container with the smallest sensible option set.\n- NO EXCEPTIONS: Do not switch to direct assistant text just because the report is long, detailed, complex, or contains multiple related items.\n- The question tool is considered available and permitted when it appears in the active tool list and the current session has not denied its use.\n- Direct assistant text is allowed only when the question tool is unavailable, denied, or absent from the current session.\n- If a report would otherwise be sent as direct text, convert it into one question batch or a short sequence of question batches instead.\n- IMPORTANT: A report-mode violation usually means the wrong tool was used, not that the report was too long.\n- IMPORTANT: Do not reinterpret a tool-usage violation as a verbosity problem.\n- Long reports are allowed. Use pagination or sequential question batches when needed; do not fall back to direct assistant text.\n- A single question tool call may contain multiple well-grouped questions. Prefer that over multiple small interruptions when it keeps the report clear.\n- Group related items into explicit batches such as current progress, key findings, decisions, and next-step choices.\n- Present the highest-priority information first and defer secondary details to later question batches when needed.\n- Even when no explicit decision is required, use brief question-tool status updates instead of direct assistant text whenever the tool is available.\n- Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.\n- MANDATORY: After any successful question-tool report, immediately choose between (a) continue unfinished non-blocked work, or (b) issue a question-tool wait-for-instruction message when no such work remains.\n- MANDATORY: Never send assistant text as a post-report fallback in either branch.\n- MANDATORY: If a post-report branch has no content, suppress assistant output and re-enter question-tool flow.\n- MANDATORY: When idle or waiting for the next task, continue using the question tool to maintain user control of the session.\n- MANDATORY: Repeated waiting for the next task is not a reason to stop using the question tool.\n- When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.\n- When the user says \"stop\", do not send assistant text to acknowledge the stop. Use the question tool only if more user-visible communication is still required by policy.\n- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.\n- Materially improves the result means clearly beneficial cases such as parallel analysis of independent areas; it does not include routine local searches, small file reads, or straightforward edits.\n- If task or subagent delegation is used, keep the number minimal and explain the reason briefly through the question tool when available.";
|
|
4
4
|
export type ExperimentalChatSystemTransformHook = (input: {
|
|
5
5
|
sessionID: string;
|
|
6
6
|
model: {
|
|
@@ -8,7 +8,8 @@ export const LOOP_SAFETY_POLICY = `Guided Loop Safety Policy
|
|
|
8
8
|
- MANDATORY: Do not reinterpret "keep working unless blocked" as permission to send direct assistant text. If you need to report, report through the question tool.
|
|
9
9
|
- MANDATORY: Do not reinterpret "don't ask unnecessary questions" as permission to skip the question tool. Use fewer question batches, not a different reporting channel.
|
|
10
10
|
- MANDATORY: If the user says a report was sent the wrong way, first check whether you used the wrong channel before analyzing verbosity, timing, or wording.
|
|
11
|
-
- MANDATORY: When reflecting on mistakes, check report-channel violations before any secondary critique.
|
|
11
|
+
- MANDATORY: When reflecting on report-delivery or tool-usage mistakes, first check report-channel violations before any secondary critique.
|
|
12
|
+
- MANDATORY: Do not reinterpret those report-channel violations as a problem with the report policy itself.
|
|
12
13
|
- MANDATORY: Do not turn a pure report into a preference question, permission question, or route-selection question just to satisfy the question tool requirement.
|
|
13
14
|
- MANDATORY: When no user decision is needed, use the question tool as a report container with the smallest sensible option set.
|
|
14
15
|
- NO EXCEPTIONS: Do not switch to direct assistant text just because the report is long, detailed, complex, or contains multiple related items.
|
|
@@ -23,6 +24,11 @@ export const LOOP_SAFETY_POLICY = `Guided Loop Safety Policy
|
|
|
23
24
|
- Present the highest-priority information first and defer secondary details to later question batches when needed.
|
|
24
25
|
- Even when no explicit decision is required, use brief question-tool status updates instead of direct assistant text whenever the tool is available.
|
|
25
26
|
- Avoid unnecessary question frequency; combine small related updates when a single question call can cover them clearly.
|
|
27
|
+
- MANDATORY: After any successful question-tool report, immediately choose between (a) continue unfinished non-blocked work, or (b) issue a question-tool wait-for-instruction message when no such work remains.
|
|
28
|
+
- MANDATORY: Never send assistant text as a post-report fallback in either branch.
|
|
29
|
+
- MANDATORY: If a post-report branch has no content, suppress assistant output and re-enter question-tool flow.
|
|
30
|
+
- MANDATORY: When idle or waiting for the next task, continue using the question tool to maintain user control of the session.
|
|
31
|
+
- MANDATORY: Repeated waiting for the next task is not a reason to stop using the question tool.
|
|
26
32
|
- When no further action can be taken safely and no non-blocked work remains, use the question tool to ask for the next task or clarification instead of ending with direct assistant text.
|
|
27
33
|
- When the user says "stop", do not send assistant text to acknowledge the stop. Use the question tool only if more user-visible communication is still required by policy.
|
|
28
34
|
- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { type CopilotPluginHooks } from "./loop-safety-plugin.js";
|
|
2
|
-
import {
|
|
2
|
+
import { type CopilotRetryContext, type FetchLike } from "./copilot-network-retry.js";
|
|
3
3
|
import { type StoreFile } from "./store.js";
|
|
4
4
|
import { type CopilotAuthState, type CopilotProviderConfig, type OfficialCopilotConfig } from "./upstream/copilot-loader-adapter.js";
|
|
5
|
+
type ChatHeadersHook = (input: {
|
|
6
|
+
sessionID: string;
|
|
7
|
+
agent: string;
|
|
8
|
+
model: {
|
|
9
|
+
providerID: string;
|
|
10
|
+
};
|
|
11
|
+
provider: {
|
|
12
|
+
source: string;
|
|
13
|
+
info: object;
|
|
14
|
+
options: object;
|
|
15
|
+
};
|
|
16
|
+
message: {
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
}, output: {
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
}) => Promise<void>;
|
|
22
|
+
type CopilotPluginHooksWithChatHeaders = CopilotPluginHooks & {
|
|
23
|
+
"chat.headers"?: ChatHeadersHook;
|
|
24
|
+
};
|
|
5
25
|
export declare function buildPluginHooks(input: {
|
|
6
26
|
auth: NonNullable<CopilotPluginHooks["auth"]>;
|
|
7
27
|
loadStore?: () => Promise<StoreFile | undefined>;
|
|
@@ -9,5 +29,9 @@ export declare function buildPluginHooks(input: {
|
|
|
9
29
|
getAuth: () => Promise<CopilotAuthState | undefined>;
|
|
10
30
|
provider?: CopilotProviderConfig;
|
|
11
31
|
}) => Promise<OfficialCopilotConfig | undefined>;
|
|
12
|
-
createRetryFetch?:
|
|
13
|
-
|
|
32
|
+
createRetryFetch?: (fetch: FetchLike, ctx?: CopilotRetryContext) => FetchLike;
|
|
33
|
+
client?: CopilotRetryContext["client"];
|
|
34
|
+
directory?: CopilotRetryContext["directory"];
|
|
35
|
+
serverUrl?: CopilotRetryContext["serverUrl"];
|
|
36
|
+
}): CopilotPluginHooksWithChatHeaders;
|
|
37
|
+
export {};
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createLoopSafetySystemTransform } from "./loop-safety-plugin.js";
|
|
2
|
-
import { createCopilotRetryingFetch } from "./copilot-network-retry.js";
|
|
1
|
+
import { createLoopSafetySystemTransform, isCopilotProvider, } from "./loop-safety-plugin.js";
|
|
2
|
+
import { createCopilotRetryingFetch, } from "./copilot-network-retry.js";
|
|
3
3
|
import { readStoreSafe } from "./store.js";
|
|
4
4
|
import { loadOfficialCopilotConfig, } from "./upstream/copilot-loader-adapter.js";
|
|
5
5
|
export function buildPluginHooks(input) {
|
|
@@ -19,15 +19,25 @@ export function buildPluginHooks(input) {
|
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
...config,
|
|
22
|
-
fetch: createRetryFetch(config.fetch
|
|
22
|
+
fetch: createRetryFetch(config.fetch, {
|
|
23
|
+
client: input.client,
|
|
24
|
+
directory: input.directory,
|
|
25
|
+
serverUrl: input.serverUrl,
|
|
26
|
+
}),
|
|
23
27
|
};
|
|
24
28
|
};
|
|
29
|
+
const chatHeaders = async (hookInput, output) => {
|
|
30
|
+
if (!isCopilotProvider(hookInput.model.providerID))
|
|
31
|
+
return;
|
|
32
|
+
output.headers["x-opencode-session-id"] = hookInput.sessionID;
|
|
33
|
+
};
|
|
25
34
|
return {
|
|
26
35
|
auth: {
|
|
27
36
|
...input.auth,
|
|
28
37
|
provider: input.auth.provider ?? "github-copilot",
|
|
29
38
|
loader,
|
|
30
39
|
},
|
|
40
|
+
"chat.headers": chatHeaders,
|
|
31
41
|
"experimental.chat.system.transform": createLoopSafetySystemTransform(loadStore),
|
|
32
42
|
};
|
|
33
43
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -443,7 +443,10 @@ async function switchAccount(client, entry) {
|
|
|
443
443
|
body: payload,
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
|
-
export const CopilotAccountSwitcher = async (
|
|
446
|
+
export const CopilotAccountSwitcher = async (input) => {
|
|
447
|
+
const client = input.client;
|
|
448
|
+
const directory = input.directory;
|
|
449
|
+
const serverUrl = input.serverUrl;
|
|
447
450
|
const methods = [
|
|
448
451
|
{
|
|
449
452
|
type: "oauth",
|
|
@@ -700,6 +703,7 @@ export const CopilotAccountSwitcher = async ({ client }) => {
|
|
|
700
703
|
store.active = name;
|
|
701
704
|
store.accounts[name].lastUsed = now();
|
|
702
705
|
await writeStore(store);
|
|
706
|
+
console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
|
|
703
707
|
continue;
|
|
704
708
|
}
|
|
705
709
|
}
|
|
@@ -709,5 +713,8 @@ export const CopilotAccountSwitcher = async ({ client }) => {
|
|
|
709
713
|
provider: "github-copilot",
|
|
710
714
|
methods,
|
|
711
715
|
},
|
|
716
|
+
client,
|
|
717
|
+
directory,
|
|
718
|
+
serverUrl,
|
|
712
719
|
});
|
|
713
720
|
};
|