opencode-copilot-account-switcher 0.2.8 → 0.3.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.
- package/dist/copilot-network-retry.d.ts +31 -0
- package/dist/copilot-network-retry.js +228 -59
- package/dist/copilot-retry-notifier.d.ts +33 -0
- package/dist/copilot-retry-notifier.js +69 -0
- package/dist/plugin-actions.d.ts +6 -0
- package/dist/plugin-actions.js +6 -0
- package/dist/plugin-hooks.d.ts +3 -0
- package/dist/plugin-hooks.js +42 -2
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.js +45 -12
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -0
- package/package.json +1 -1
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
export type FetchLike = (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
export type CopilotRetryNotifier = {
|
|
3
|
+
started: (state: {
|
|
4
|
+
remaining: number;
|
|
5
|
+
}) => Promise<void>;
|
|
6
|
+
progress: (state: {
|
|
7
|
+
remaining: number;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
repairWarning: (state: {
|
|
10
|
+
remaining: number;
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
completed: (state: {
|
|
13
|
+
remaining: number;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
stopped: (state: {
|
|
16
|
+
remaining: number;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
};
|
|
2
19
|
type JsonRecord = Record<string, unknown>;
|
|
3
20
|
export type CopilotRetryContext = {
|
|
4
21
|
client?: {
|
|
@@ -27,14 +44,28 @@ export type CopilotRetryContext = {
|
|
|
27
44
|
};
|
|
28
45
|
}>;
|
|
29
46
|
};
|
|
47
|
+
tui?: {
|
|
48
|
+
showToast?: (options: {
|
|
49
|
+
body: {
|
|
50
|
+
title?: string;
|
|
51
|
+
message: string;
|
|
52
|
+
variant: "info" | "success" | "warning" | "error";
|
|
53
|
+
duration?: number;
|
|
54
|
+
};
|
|
55
|
+
query?: undefined;
|
|
56
|
+
}) => Promise<unknown>;
|
|
57
|
+
};
|
|
30
58
|
};
|
|
31
59
|
directory?: string;
|
|
32
60
|
serverUrl?: URL;
|
|
61
|
+
lastAccountSwitchAt?: number;
|
|
62
|
+
clearAccountSwitchContext?: () => Promise<void>;
|
|
33
63
|
wait?: (ms: number) => Promise<void>;
|
|
34
64
|
patchPart?: (request: {
|
|
35
65
|
url: string;
|
|
36
66
|
init: RequestInit;
|
|
37
67
|
}) => Promise<unknown>;
|
|
68
|
+
notifier?: CopilotRetryNotifier;
|
|
38
69
|
};
|
|
39
70
|
export declare function isRetryableCopilotFetchError(error: unknown): boolean;
|
|
40
71
|
export declare function createCopilotRetryingFetch(baseFetch: FetchLike, options?: CopilotRetryContext): (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
|
|
@@ -13,7 +13,6 @@ 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
16
|
const INTERNAL_SESSION_HEADER = "x-opencode-session-id";
|
|
18
17
|
const defaultDebugLogFile = (() => {
|
|
19
18
|
const tmp = process.env.TEMP || process.env.TMP || "/tmp";
|
|
@@ -110,34 +109,64 @@ function collectLongInputIdCandidates(payload) {
|
|
|
110
109
|
return [{ item: item, payloadIndex, idLength: id.length }];
|
|
111
110
|
});
|
|
112
111
|
}
|
|
113
|
-
function
|
|
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) {
|
|
112
|
+
function getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength) {
|
|
120
113
|
const matches = collectLongInputIdCandidates(payload);
|
|
121
|
-
if (matches.length === 0)
|
|
122
|
-
return
|
|
114
|
+
if (matches.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
strategy: "ambiguous",
|
|
117
|
+
candidates: [],
|
|
118
|
+
reportedLengthMatched: reportedLength === undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
123
121
|
const lengthMatches = reportedLength
|
|
124
122
|
? matches.filter((item) => item.idLength === reportedLength)
|
|
125
123
|
: matches;
|
|
126
|
-
if (lengthMatches.length ===
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
if (reportedLength !== undefined && lengthMatches.length === 0) {
|
|
125
|
+
return {
|
|
126
|
+
strategy: "ambiguous",
|
|
127
|
+
candidates: [],
|
|
128
|
+
reportedLengthMatched: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (lengthMatches.length === 1) {
|
|
132
|
+
return {
|
|
133
|
+
candidate: lengthMatches[0],
|
|
134
|
+
strategy: reportedLength !== undefined && matches.length > 1 ? "reported-length" : "single-long-id",
|
|
135
|
+
candidates: lengthMatches,
|
|
136
|
+
reportedLengthMatched: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (matches.length === 1) {
|
|
140
|
+
return {
|
|
141
|
+
candidate: matches[0],
|
|
142
|
+
strategy: "single-long-id",
|
|
143
|
+
candidates: matches,
|
|
144
|
+
reportedLengthMatched: reportedLength === undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
130
147
|
const narrowedCandidates = lengthMatches.length > 0 ? lengthMatches : matches;
|
|
131
148
|
if (typeof serverReportedIndex === "number") {
|
|
132
|
-
|
|
149
|
+
const hintedCandidates = narrowedCandidates.filter((candidate) => candidate.payloadIndex === serverReportedIndex || candidate.payloadIndex + 1 === serverReportedIndex);
|
|
150
|
+
if (hintedCandidates.length === 1) {
|
|
151
|
+
return {
|
|
152
|
+
candidate: hintedCandidates[0],
|
|
153
|
+
strategy: "index-hint",
|
|
154
|
+
candidates: narrowedCandidates,
|
|
155
|
+
reportedLengthMatched: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
133
158
|
}
|
|
134
|
-
return
|
|
159
|
+
return {
|
|
160
|
+
strategy: "ambiguous",
|
|
161
|
+
candidates: narrowedCandidates,
|
|
162
|
+
reportedLengthMatched: true,
|
|
163
|
+
};
|
|
135
164
|
}
|
|
136
165
|
function stripTargetedLongInputId(payload, serverReportedIndex, reportedLength) {
|
|
137
166
|
const input = payload.input;
|
|
138
167
|
if (!Array.isArray(input))
|
|
139
168
|
return payload;
|
|
140
|
-
const target =
|
|
169
|
+
const target = getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength).candidate?.item;
|
|
141
170
|
if (!target)
|
|
142
171
|
return payload;
|
|
143
172
|
let changed = false;
|
|
@@ -181,6 +210,21 @@ function buildRetryInit(init, payload) {
|
|
|
181
210
|
body: JSON.stringify(payload),
|
|
182
211
|
};
|
|
183
212
|
}
|
|
213
|
+
const noopNotifier = {
|
|
214
|
+
started: async () => { },
|
|
215
|
+
progress: async () => { },
|
|
216
|
+
repairWarning: async () => { },
|
|
217
|
+
completed: async () => { },
|
|
218
|
+
stopped: async () => { },
|
|
219
|
+
};
|
|
220
|
+
async function notify(notifier, event, remaining) {
|
|
221
|
+
try {
|
|
222
|
+
await notifier[event]({ remaining });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.warn(`[copilot-network-retry] notifier ${event} failed`, error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
184
228
|
function stripInternalSessionHeaderFromRequest(request) {
|
|
185
229
|
if (!(request instanceof Request))
|
|
186
230
|
return request;
|
|
@@ -200,12 +244,18 @@ function getHeader(request, init, name) {
|
|
|
200
244
|
return undefined;
|
|
201
245
|
}
|
|
202
246
|
function getTargetedInputId(payload, serverReportedIndex, reportedLength) {
|
|
203
|
-
const target =
|
|
247
|
+
const target = getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength).candidate?.item;
|
|
204
248
|
const id = target?.id;
|
|
205
249
|
if (typeof id !== "string")
|
|
206
250
|
return undefined;
|
|
207
251
|
return id;
|
|
208
252
|
}
|
|
253
|
+
function logCleanupStopped(reason, details) {
|
|
254
|
+
debugLog("input-id retry cleanup-stopped", {
|
|
255
|
+
reason,
|
|
256
|
+
...(details ?? {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
209
259
|
function stripOpenAIItemId(part) {
|
|
210
260
|
const metadata = part.metadata;
|
|
211
261
|
if (!metadata || typeof metadata !== "object")
|
|
@@ -316,14 +366,25 @@ async function repairSessionPart(sessionID, failingId, ctx) {
|
|
|
316
366
|
return false;
|
|
317
367
|
}
|
|
318
368
|
}
|
|
319
|
-
async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx, sessionID) {
|
|
369
|
+
async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
|
|
320
370
|
if (response.status !== 400) {
|
|
321
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
response,
|
|
373
|
+
retried: false,
|
|
374
|
+
nextInit: init,
|
|
375
|
+
nextPayload: requestPayload,
|
|
376
|
+
retryState: undefined,
|
|
377
|
+
};
|
|
322
378
|
}
|
|
323
|
-
const requestPayload = parseJsonBody(init);
|
|
324
379
|
if (!requestPayload || !hasLongInputIds(requestPayload)) {
|
|
325
380
|
debugLog("skip input-id retry: request has no long ids");
|
|
326
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
response,
|
|
383
|
+
retried: false,
|
|
384
|
+
nextInit: init,
|
|
385
|
+
nextPayload: requestPayload,
|
|
386
|
+
retryState: undefined,
|
|
387
|
+
};
|
|
327
388
|
}
|
|
328
389
|
debugLog("input-id retry candidate", {
|
|
329
390
|
status: response.status,
|
|
@@ -335,7 +396,13 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
335
396
|
.catch(() => "");
|
|
336
397
|
if (!responseText) {
|
|
337
398
|
debugLog("skip input-id retry: empty response body");
|
|
338
|
-
return {
|
|
399
|
+
return {
|
|
400
|
+
response,
|
|
401
|
+
retried: false,
|
|
402
|
+
nextInit: init,
|
|
403
|
+
nextPayload: requestPayload,
|
|
404
|
+
retryState: undefined,
|
|
405
|
+
};
|
|
339
406
|
}
|
|
340
407
|
let parsed = parseInputIdTooLongDetails(responseText);
|
|
341
408
|
let matched = parsed.matched;
|
|
@@ -361,31 +428,61 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
361
428
|
reportedLength: parsed.reportedLength,
|
|
362
429
|
});
|
|
363
430
|
if (!matched) {
|
|
364
|
-
return {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
431
|
+
return {
|
|
432
|
+
response,
|
|
433
|
+
retried: false,
|
|
434
|
+
nextInit: init,
|
|
435
|
+
nextPayload: requestPayload,
|
|
436
|
+
retryState: undefined,
|
|
437
|
+
};
|
|
371
438
|
}
|
|
372
439
|
const payloadCandidates = getPayloadCandidates(requestPayload);
|
|
440
|
+
const targetSelection = getTargetedLongInputIdSelection(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
373
441
|
debugLog("input-id retry payload candidates", {
|
|
374
442
|
serverReportedIndex: parsed.serverReportedIndex,
|
|
375
443
|
candidates: payloadCandidates,
|
|
376
444
|
});
|
|
377
445
|
const failingId = getTargetedInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
378
|
-
const targetedPayload =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
446
|
+
const targetedPayload = targetSelection.candidate
|
|
447
|
+
? payloadCandidates.find((item) => item.payloadIndex === targetSelection.candidate?.payloadIndex)
|
|
448
|
+
: undefined;
|
|
449
|
+
debugLog("input-id retry payload target", {
|
|
450
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
451
|
+
targetedPayloadIndex: targetedPayload?.payloadIndex,
|
|
452
|
+
itemKind: targetedPayload?.itemKind,
|
|
453
|
+
idLength: targetedPayload?.idLength,
|
|
454
|
+
idPreview: targetedPayload?.idPreview,
|
|
455
|
+
strategy: targetSelection.strategy,
|
|
456
|
+
});
|
|
457
|
+
const remainingBefore = countLongInputIdCandidates(requestPayload);
|
|
458
|
+
if (!targetSelection.candidate) {
|
|
459
|
+
logCleanupStopped("evidence-insufficient", {
|
|
460
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
461
|
+
reportedLength: parsed.reportedLength,
|
|
462
|
+
candidateCount: targetSelection.candidates.length,
|
|
463
|
+
reportedLengthMatched: targetSelection.reportedLengthMatched,
|
|
385
464
|
});
|
|
465
|
+
return {
|
|
466
|
+
response,
|
|
467
|
+
retried: false,
|
|
468
|
+
nextInit: init,
|
|
469
|
+
nextPayload: requestPayload,
|
|
470
|
+
retryState: {
|
|
471
|
+
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
472
|
+
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
473
|
+
remainingLongIdCandidatesBefore: remainingBefore,
|
|
474
|
+
remainingLongIdCandidatesAfter: remainingBefore,
|
|
475
|
+
previousReportedLength: parsed.reportedLength,
|
|
476
|
+
notifiedStarted: startedNotified || remainingBefore > 0,
|
|
477
|
+
repairFailed: false,
|
|
478
|
+
stopReason: "evidence-insufficient",
|
|
479
|
+
},
|
|
480
|
+
};
|
|
386
481
|
}
|
|
482
|
+
const notifiedStarted = startedNotified || remainingBefore > 0;
|
|
483
|
+
let repairFailed = false;
|
|
387
484
|
if (sessionID && failingId) {
|
|
388
|
-
await repairSessionPart(sessionID, failingId, ctx).catch(() => false);
|
|
485
|
+
repairFailed = !(await repairSessionPart(sessionID, failingId, ctx).catch(() => false));
|
|
389
486
|
}
|
|
390
487
|
const sanitized = stripTargetedLongInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
391
488
|
if (sanitized === requestPayload) {
|
|
@@ -394,11 +491,15 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
394
491
|
response,
|
|
395
492
|
retried: false,
|
|
396
493
|
nextInit: init,
|
|
494
|
+
nextPayload: requestPayload,
|
|
397
495
|
retryState: {
|
|
398
496
|
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
399
497
|
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
400
|
-
remainingLongIdCandidatesBefore:
|
|
401
|
-
remainingLongIdCandidatesAfter:
|
|
498
|
+
remainingLongIdCandidatesBefore: remainingBefore,
|
|
499
|
+
remainingLongIdCandidatesAfter: remainingBefore,
|
|
500
|
+
previousReportedLength: parsed.reportedLength,
|
|
501
|
+
notifiedStarted,
|
|
502
|
+
repairFailed,
|
|
402
503
|
},
|
|
403
504
|
};
|
|
404
505
|
}
|
|
@@ -411,14 +512,17 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
411
512
|
const retryState = {
|
|
412
513
|
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
413
514
|
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
414
|
-
remainingLongIdCandidatesBefore:
|
|
515
|
+
remainingLongIdCandidatesBefore: remainingBefore,
|
|
415
516
|
remainingLongIdCandidatesAfter: countLongInputIdCandidates(parseJsonBody(nextInit)),
|
|
517
|
+
previousReportedLength: parsed.reportedLength,
|
|
518
|
+
notifiedStarted,
|
|
519
|
+
repairFailed,
|
|
416
520
|
};
|
|
417
521
|
debugLog("input-id retry response", {
|
|
418
522
|
status: retried.status,
|
|
419
523
|
contentType: retried.headers.get("content-type") ?? undefined,
|
|
420
524
|
});
|
|
421
|
-
return { response: retried, retried: true, nextInit, retryState };
|
|
525
|
+
return { response: retried, retried: true, nextInit, nextPayload: sanitized, retryState };
|
|
422
526
|
}
|
|
423
527
|
function toRetryableSystemError(error) {
|
|
424
528
|
const base = error instanceof Error ? error : new Error(String(error));
|
|
@@ -468,9 +572,29 @@ async function getInputIdRetryErrorDetails(response) {
|
|
|
468
572
|
return undefined;
|
|
469
573
|
return {
|
|
470
574
|
serverReportedIndex: parsed.serverReportedIndex,
|
|
575
|
+
reportedLength: parsed.reportedLength,
|
|
471
576
|
errorMessagePreview: buildMessagePreview(message),
|
|
472
577
|
};
|
|
473
578
|
}
|
|
579
|
+
async function parseJsonRequestPayload(request, init) {
|
|
580
|
+
const initPayload = parseJsonBody(init);
|
|
581
|
+
if (initPayload)
|
|
582
|
+
return initPayload;
|
|
583
|
+
if (!(request instanceof Request))
|
|
584
|
+
return undefined;
|
|
585
|
+
try {
|
|
586
|
+
const body = await request.clone().text();
|
|
587
|
+
if (!body)
|
|
588
|
+
return undefined;
|
|
589
|
+
const parsed = JSON.parse(body);
|
|
590
|
+
if (!parsed || typeof parsed !== "object")
|
|
591
|
+
return undefined;
|
|
592
|
+
return parsed;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
474
598
|
function withStreamDebugLogs(response, request) {
|
|
475
599
|
if (!isDebugEnabled())
|
|
476
600
|
return response;
|
|
@@ -519,7 +643,7 @@ export function isRetryableCopilotFetchError(error) {
|
|
|
519
643
|
return RETRYABLE_MESSAGES.some((part) => message.includes(part));
|
|
520
644
|
}
|
|
521
645
|
export function createCopilotRetryingFetch(baseFetch, options) {
|
|
522
|
-
|
|
646
|
+
const notifier = options?.notifier ?? noopNotifier;
|
|
523
647
|
return async function retryingFetch(request, init) {
|
|
524
648
|
const sessionID = getHeader(request, init, INTERNAL_SESSION_HEADER);
|
|
525
649
|
const safeRequest = stripInternalSessionHeaderFromRequest(request);
|
|
@@ -535,6 +659,7 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
535
659
|
url: safeRequest instanceof Request ? safeRequest.url : safeRequest instanceof URL ? safeRequest.href : String(safeRequest),
|
|
536
660
|
isCopilot: isCopilotUrl(safeRequest),
|
|
537
661
|
});
|
|
662
|
+
let currentPayload = await parseJsonRequestPayload(safeRequest, effectiveInit);
|
|
538
663
|
try {
|
|
539
664
|
const response = await baseFetch(safeRequest, effectiveInit);
|
|
540
665
|
debugLog("fetch resolved", {
|
|
@@ -545,27 +670,42 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
545
670
|
let currentResponse = response;
|
|
546
671
|
let currentInit = effectiveInit;
|
|
547
672
|
let attempts = 0;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
673
|
+
let shouldContinueInputIdRepair = countLongInputIdCandidates(currentPayload) > 0;
|
|
674
|
+
let startedNotified = false;
|
|
675
|
+
let finishedNotified = false;
|
|
676
|
+
let repairWarningNotified = false;
|
|
677
|
+
while (shouldContinueInputIdRepair) {
|
|
678
|
+
shouldContinueInputIdRepair = false;
|
|
679
|
+
const result = await maybeRetryInputIdTooLong(safeRequest, currentInit, currentResponse, baseFetch, currentPayload, options, sessionID, startedNotified);
|
|
553
680
|
currentResponse = result.response;
|
|
554
681
|
currentInit = result.nextInit;
|
|
682
|
+
currentPayload = result.nextPayload;
|
|
555
683
|
if (result.retryState) {
|
|
684
|
+
if (!startedNotified && result.retryState.notifiedStarted) {
|
|
685
|
+
startedNotified = true;
|
|
686
|
+
await notify(notifier, "started", result.retryState.remainingLongIdCandidatesBefore);
|
|
687
|
+
}
|
|
688
|
+
if (result.retryState.repairFailed && !repairWarningNotified) {
|
|
689
|
+
await notify(notifier, "repairWarning", result.retryState.remainingLongIdCandidatesBefore);
|
|
690
|
+
repairWarningNotified = true;
|
|
691
|
+
}
|
|
556
692
|
const currentError = await getInputIdRetryErrorDetails(currentResponse);
|
|
557
|
-
let stopReason;
|
|
558
|
-
|
|
693
|
+
let stopReason = result.retryState.stopReason;
|
|
694
|
+
const madeProgress = result.retryState.remainingLongIdCandidatesAfter < result.retryState.remainingLongIdCandidatesBefore;
|
|
695
|
+
if (!stopReason && result.retryState.remainingLongIdCandidatesAfter >= result.retryState.remainingLongIdCandidatesBefore) {
|
|
559
696
|
stopReason = "remaining-candidates-not-reduced";
|
|
560
697
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
698
|
+
if (!stopReason &&
|
|
699
|
+
currentError &&
|
|
700
|
+
result.retryState.remainingLongIdCandidatesAfter > 0 &&
|
|
701
|
+
result.retryState.previousServerReportedIndex === currentError.serverReportedIndex &&
|
|
702
|
+
result.retryState.previousReportedLength === currentError.reportedLength) {
|
|
703
|
+
stopReason = "same-server-item-persists";
|
|
567
704
|
}
|
|
568
|
-
if (currentError
|
|
705
|
+
if (!stopReason && currentError && result.retryState.remainingLongIdCandidatesAfter === 0) {
|
|
706
|
+
stopReason = "local-candidates-exhausted";
|
|
707
|
+
}
|
|
708
|
+
if ((currentError || stopReason) && stopReason !== "evidence-insufficient") {
|
|
569
709
|
debugLog("input-id retry progress", {
|
|
570
710
|
attempt: attempts + 1,
|
|
571
711
|
previousServerReportedIndex: result.retryState.previousServerReportedIndex,
|
|
@@ -578,11 +718,40 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
578
718
|
stopReason,
|
|
579
719
|
});
|
|
580
720
|
}
|
|
581
|
-
if (stopReason)
|
|
721
|
+
if (stopReason === "local-candidates-exhausted") {
|
|
722
|
+
logCleanupStopped("local-candidates-exhausted", {
|
|
723
|
+
attempt: attempts + 1,
|
|
724
|
+
previousServerReportedIndex: result.retryState.previousServerReportedIndex,
|
|
725
|
+
currentServerReportedIndex: currentError?.serverReportedIndex,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (stopReason) {
|
|
729
|
+
await notify(notifier, "stopped", result.retryState.remainingLongIdCandidatesAfter);
|
|
730
|
+
finishedNotified = true;
|
|
582
731
|
break;
|
|
732
|
+
}
|
|
733
|
+
if (result.retried && madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0) {
|
|
734
|
+
await notify(notifier, "progress", result.retryState.remainingLongIdCandidatesAfter);
|
|
735
|
+
}
|
|
736
|
+
if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && currentResponse.ok) {
|
|
737
|
+
await notify(notifier, "completed", 0);
|
|
738
|
+
finishedNotified = true;
|
|
739
|
+
}
|
|
740
|
+
if (result.retried && result.retryState.remainingLongIdCandidatesAfter === 0 && !currentResponse.ok) {
|
|
741
|
+
await notify(notifier, "stopped", 0);
|
|
742
|
+
finishedNotified = true;
|
|
743
|
+
}
|
|
744
|
+
if (result.retried && madeProgress && result.retryState.remainingLongIdCandidatesAfter > 0) {
|
|
745
|
+
shouldContinueInputIdRepair = true;
|
|
746
|
+
}
|
|
583
747
|
}
|
|
584
|
-
if (!result.retried)
|
|
748
|
+
if (!result.retried) {
|
|
749
|
+
if (startedNotified && !finishedNotified) {
|
|
750
|
+
await notify(notifier, "stopped", countLongInputIdCandidates(currentPayload));
|
|
751
|
+
finishedNotified = true;
|
|
752
|
+
}
|
|
585
753
|
break;
|
|
754
|
+
}
|
|
586
755
|
attempts += 1;
|
|
587
756
|
}
|
|
588
757
|
return withStreamDebugLogs(currentResponse, safeRequest);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const ACCOUNT_SWITCH_TTL_MS: number;
|
|
2
|
+
type ToastVariant = "info" | "success" | "warning" | "error";
|
|
3
|
+
type RetryToastState = {
|
|
4
|
+
remaining: number;
|
|
5
|
+
};
|
|
6
|
+
type RetryToastClient = {
|
|
7
|
+
tui?: {
|
|
8
|
+
showToast?: (options: {
|
|
9
|
+
body: {
|
|
10
|
+
title?: string;
|
|
11
|
+
message: string;
|
|
12
|
+
variant: ToastVariant;
|
|
13
|
+
duration?: number;
|
|
14
|
+
};
|
|
15
|
+
query?: undefined;
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
type RetryNotifierContext = {
|
|
20
|
+
client?: RetryToastClient;
|
|
21
|
+
lastAccountSwitchAt?: number;
|
|
22
|
+
getLastAccountSwitchAt?: () => Promise<number | undefined> | number | undefined;
|
|
23
|
+
clearAccountSwitchContext?: (lastAccountSwitchAt?: number) => Promise<void>;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
};
|
|
26
|
+
export declare function createCopilotRetryNotifier(ctx: RetryNotifierContext): {
|
|
27
|
+
started: (state: RetryToastState) => Promise<void>;
|
|
28
|
+
progress: (state: RetryToastState) => Promise<void>;
|
|
29
|
+
repairWarning: (state: RetryToastState) => Promise<void>;
|
|
30
|
+
completed: (state: RetryToastState) => Promise<void>;
|
|
31
|
+
stopped: (state: RetryToastState) => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const ACCOUNT_SWITCH_TTL_MS = 30 * 60 * 1000;
|
|
2
|
+
function buildPrefix(lastAccountSwitchAt) {
|
|
3
|
+
if (typeof lastAccountSwitchAt !== "number")
|
|
4
|
+
return "Copilot 输入 ID 自动清理中";
|
|
5
|
+
return "正在清理可能因账号切换遗留的非法输入 ID";
|
|
6
|
+
}
|
|
7
|
+
async function resolveToastAccountSwitchAt(ctx) {
|
|
8
|
+
if (ctx.lastAccountSwitchAt !== undefined && ctx.lastAccountSwitchAt !== null) {
|
|
9
|
+
return ctx.lastAccountSwitchAt;
|
|
10
|
+
}
|
|
11
|
+
return ctx.getLastAccountSwitchAt?.();
|
|
12
|
+
}
|
|
13
|
+
function isAccountSwitchContextExpired(lastAccountSwitchAt, now) {
|
|
14
|
+
if (typeof lastAccountSwitchAt !== "number")
|
|
15
|
+
return false;
|
|
16
|
+
return now() - lastAccountSwitchAt >= ACCOUNT_SWITCH_TTL_MS;
|
|
17
|
+
}
|
|
18
|
+
async function clearContext(ctx) {
|
|
19
|
+
try {
|
|
20
|
+
await ctx.clearAccountSwitchContext?.();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.warn("[copilot-retry-notifier] failed to clear account-switch context", error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createCopilotRetryNotifier(ctx) {
|
|
27
|
+
let lastExpiredContextClearedAt;
|
|
28
|
+
async function send(variant, detail, state, clear = false) {
|
|
29
|
+
const now = ctx.now ?? Date.now;
|
|
30
|
+
const lastAccountSwitchAt = await resolveToastAccountSwitchAt(ctx);
|
|
31
|
+
try {
|
|
32
|
+
await ctx.client?.tui?.showToast?.({
|
|
33
|
+
body: {
|
|
34
|
+
variant,
|
|
35
|
+
message: `${buildPrefix(lastAccountSwitchAt)}:${detail},剩余 ${state.remaining} 项。`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn("[copilot-retry-notifier] failed to show toast", error);
|
|
41
|
+
}
|
|
42
|
+
if (!clear
|
|
43
|
+
&& isAccountSwitchContextExpired(lastAccountSwitchAt, now)
|
|
44
|
+
&& lastAccountSwitchAt !== lastExpiredContextClearedAt) {
|
|
45
|
+
lastExpiredContextClearedAt = lastAccountSwitchAt;
|
|
46
|
+
await clearContext({
|
|
47
|
+
...ctx,
|
|
48
|
+
clearAccountSwitchContext: async () => {
|
|
49
|
+
await ctx.clearAccountSwitchContext?.(lastAccountSwitchAt);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!clear)
|
|
54
|
+
return;
|
|
55
|
+
await clearContext({
|
|
56
|
+
...ctx,
|
|
57
|
+
clearAccountSwitchContext: async () => {
|
|
58
|
+
await ctx.clearAccountSwitchContext?.(lastAccountSwitchAt);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
started: async (state) => send("info", "已开始自动清理", state),
|
|
64
|
+
progress: async (state) => send("info", "自动清理仍在继续", state),
|
|
65
|
+
repairWarning: async (state) => send("warning", "会话回写失败,继续尝试仅清理请求体", state),
|
|
66
|
+
completed: async (state) => send("success", "自动清理已完成", state, true),
|
|
67
|
+
stopped: async (state) => send("warning", "自动清理已停止", state, true),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/plugin-actions.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { StoreFile } from "./store.js";
|
|
2
2
|
import type { MenuAction } from "./ui/menu.js";
|
|
3
|
+
export declare function persistAccountSwitch(input: {
|
|
4
|
+
store: StoreFile;
|
|
5
|
+
name: string;
|
|
6
|
+
at: number;
|
|
7
|
+
writeStore: (store: StoreFile) => Promise<void>;
|
|
8
|
+
}): Promise<void>;
|
|
3
9
|
export declare function applyMenuAction(input: {
|
|
4
10
|
action: MenuAction;
|
|
5
11
|
store: StoreFile;
|
package/dist/plugin-actions.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export async function persistAccountSwitch(input) {
|
|
2
|
+
input.store.active = input.name;
|
|
3
|
+
input.store.accounts[input.name].lastUsed = input.at;
|
|
4
|
+
input.store.lastAccountSwitchAt = input.at;
|
|
5
|
+
await input.writeStore(input.store);
|
|
6
|
+
}
|
|
1
7
|
export async function applyMenuAction(input) {
|
|
2
8
|
if (input.action.type === "toggle-loop-safety") {
|
|
3
9
|
input.store.loopSafetyEnabled = input.store.loopSafetyEnabled !== true;
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ type CopilotPluginHooksWithChatHeaders = CopilotPluginHooks & {
|
|
|
25
25
|
export declare function buildPluginHooks(input: {
|
|
26
26
|
auth: NonNullable<CopilotPluginHooks["auth"]>;
|
|
27
27
|
loadStore?: () => Promise<StoreFile | undefined>;
|
|
28
|
+
writeStore?: (store: StoreFile) => Promise<void>;
|
|
28
29
|
loadOfficialConfig?: (input: {
|
|
29
30
|
getAuth: () => Promise<CopilotAuthState | undefined>;
|
|
30
31
|
provider?: CopilotProviderConfig;
|
|
@@ -33,5 +34,7 @@ export declare function buildPluginHooks(input: {
|
|
|
33
34
|
client?: CopilotRetryContext["client"];
|
|
34
35
|
directory?: CopilotRetryContext["directory"];
|
|
35
36
|
serverUrl?: CopilotRetryContext["serverUrl"];
|
|
37
|
+
clearAccountSwitchContext?: (lastAccountSwitchAt?: number) => Promise<void>;
|
|
38
|
+
now?: () => number;
|
|
36
39
|
}): CopilotPluginHooksWithChatHeaders;
|
|
37
40
|
export {};
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
import { createLoopSafetySystemTransform, isCopilotProvider, } from "./loop-safety-plugin.js";
|
|
2
2
|
import { createCopilotRetryingFetch, } from "./copilot-network-retry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createCopilotRetryNotifier } from "./copilot-retry-notifier.js";
|
|
4
|
+
import { readStoreSafe, writeStore } from "./store.js";
|
|
4
5
|
import { loadOfficialCopilotConfig, } from "./upstream/copilot-loader-adapter.js";
|
|
6
|
+
function readRetryStoreContext(store) {
|
|
7
|
+
if (!store)
|
|
8
|
+
return undefined;
|
|
9
|
+
const maybeLastAccountSwitchAt = store.lastAccountSwitchAt;
|
|
10
|
+
return {
|
|
11
|
+
networkRetryEnabled: store.networkRetryEnabled,
|
|
12
|
+
lastAccountSwitchAt: typeof maybeLastAccountSwitchAt === "number" ? maybeLastAccountSwitchAt : undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
5
15
|
export function buildPluginHooks(input) {
|
|
6
16
|
const loadStore = input.loadStore ?? readStoreSafe;
|
|
17
|
+
const persistStore = input.writeStore ?? writeStore;
|
|
7
18
|
const loadOfficialConfig = input.loadOfficialConfig ?? loadOfficialCopilotConfig;
|
|
8
19
|
const createRetryFetch = input.createRetryFetch ?? createCopilotRetryingFetch;
|
|
20
|
+
const getLatestLastAccountSwitchAt = async () => {
|
|
21
|
+
const store = readRetryStoreContext(await loadStore().catch(() => undefined));
|
|
22
|
+
return store?.lastAccountSwitchAt;
|
|
23
|
+
};
|
|
24
|
+
const clearAccountSwitchContext = input.clearAccountSwitchContext ?? (async (capturedLastAccountSwitchAt) => {
|
|
25
|
+
if (capturedLastAccountSwitchAt === undefined)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const latestStore = await loadStore().catch(() => undefined);
|
|
29
|
+
if (!latestStore)
|
|
30
|
+
return;
|
|
31
|
+
if (latestStore.lastAccountSwitchAt !== capturedLastAccountSwitchAt)
|
|
32
|
+
return;
|
|
33
|
+
delete latestStore.lastAccountSwitchAt;
|
|
34
|
+
await persistStore(latestStore);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn("[plugin-hooks] failed to clear account-switch context", error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
9
40
|
const loader = async (getAuth, provider) => {
|
|
10
41
|
const config = await loadOfficialConfig({
|
|
11
42
|
getAuth: getAuth,
|
|
@@ -13,7 +44,7 @@ export function buildPluginHooks(input) {
|
|
|
13
44
|
});
|
|
14
45
|
if (!config)
|
|
15
46
|
return {};
|
|
16
|
-
const store = await loadStore().catch(() => undefined);
|
|
47
|
+
const store = readRetryStoreContext(await loadStore().catch(() => undefined));
|
|
17
48
|
if (store?.networkRetryEnabled !== true) {
|
|
18
49
|
return config;
|
|
19
50
|
}
|
|
@@ -23,6 +54,15 @@ export function buildPluginHooks(input) {
|
|
|
23
54
|
client: input.client,
|
|
24
55
|
directory: input.directory,
|
|
25
56
|
serverUrl: input.serverUrl,
|
|
57
|
+
lastAccountSwitchAt: store.lastAccountSwitchAt,
|
|
58
|
+
notifier: createCopilotRetryNotifier({
|
|
59
|
+
client: input.client,
|
|
60
|
+
lastAccountSwitchAt: store.lastAccountSwitchAt,
|
|
61
|
+
getLastAccountSwitchAt: getLatestLastAccountSwitchAt,
|
|
62
|
+
clearAccountSwitchContext,
|
|
63
|
+
now: input.now,
|
|
64
|
+
}),
|
|
65
|
+
clearAccountSwitchContext: async () => clearAccountSwitchContext(store.lastAccountSwitchAt),
|
|
26
66
|
}),
|
|
27
67
|
};
|
|
28
68
|
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { type StoreFile } from "./store.js";
|
|
3
|
+
export declare function activateAddedAccount(input: {
|
|
4
|
+
store: StoreFile;
|
|
5
|
+
name: string;
|
|
6
|
+
switchAccount: () => Promise<void>;
|
|
7
|
+
writeStore: (store: StoreFile) => Promise<void>;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
}): Promise<void>;
|
|
2
10
|
export declare const CopilotAccountSwitcher: Plugin;
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
-
import { applyMenuAction } from "./plugin-actions.js";
|
|
3
|
+
import { applyMenuAction, persistAccountSwitch } from "./plugin-actions.js";
|
|
4
4
|
import { buildPluginHooks } from "./plugin-hooks.js";
|
|
5
5
|
import { isTTY } from "./ui/ansi.js";
|
|
6
6
|
import { showAccountActions, showMenu } from "./ui/menu.js";
|
|
@@ -443,6 +443,16 @@ async function switchAccount(client, entry) {
|
|
|
443
443
|
body: payload,
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
|
+
export async function activateAddedAccount(input) {
|
|
447
|
+
await input.writeStore(input.store);
|
|
448
|
+
await input.switchAccount();
|
|
449
|
+
await persistAccountSwitch({
|
|
450
|
+
store: input.store,
|
|
451
|
+
name: input.name,
|
|
452
|
+
at: (input.now ?? now)(),
|
|
453
|
+
writeStore: input.writeStore,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
446
456
|
export const CopilotAccountSwitcher = async (input) => {
|
|
447
457
|
const client = input.client;
|
|
448
458
|
const directory = input.directory;
|
|
@@ -487,8 +497,12 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
487
497
|
const { name, entry } = await promptAccountEntry([]);
|
|
488
498
|
store.accounts[name] = entry;
|
|
489
499
|
store.active = name;
|
|
490
|
-
await
|
|
491
|
-
|
|
500
|
+
await activateAddedAccount({
|
|
501
|
+
store,
|
|
502
|
+
name,
|
|
503
|
+
switchAccount: () => switchAccount(client, entry),
|
|
504
|
+
writeStore,
|
|
505
|
+
});
|
|
492
506
|
// fallthrough to menu
|
|
493
507
|
}
|
|
494
508
|
if (!Object.values(store.accounts).some((entry) => entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))) {
|
|
@@ -590,9 +604,17 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
590
604
|
entry.name = buildName(entry, user?.login);
|
|
591
605
|
store.accounts[entry.name] = entry;
|
|
592
606
|
store.active = store.active ?? entry.name;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
607
|
+
if (store.active === entry.name) {
|
|
608
|
+
await activateAddedAccount({
|
|
609
|
+
store,
|
|
610
|
+
name: entry.name,
|
|
611
|
+
switchAccount: () => switchAccount(client, entry),
|
|
612
|
+
writeStore,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
await writeStore(store);
|
|
617
|
+
}
|
|
596
618
|
continue;
|
|
597
619
|
}
|
|
598
620
|
const manual = await promptAccountEntry(Object.keys(store.accounts));
|
|
@@ -606,9 +628,17 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
606
628
|
manual.entry.name = buildName(manual.entry, user?.login);
|
|
607
629
|
store.accounts[manual.entry.name] = manual.entry;
|
|
608
630
|
store.active = store.active ?? manual.entry.name;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
631
|
+
if (store.active === manual.entry.name) {
|
|
632
|
+
await activateAddedAccount({
|
|
633
|
+
store,
|
|
634
|
+
name: manual.entry.name,
|
|
635
|
+
switchAccount: () => switchAccount(client, manual.entry),
|
|
636
|
+
writeStore,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
await writeStore(store);
|
|
641
|
+
}
|
|
612
642
|
continue;
|
|
613
643
|
}
|
|
614
644
|
if (action.type === "import") {
|
|
@@ -700,9 +730,12 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
700
730
|
continue;
|
|
701
731
|
}
|
|
702
732
|
await switchAccount(client, entry);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
733
|
+
await persistAccountSwitch({
|
|
734
|
+
store,
|
|
735
|
+
name,
|
|
736
|
+
at: now(),
|
|
737
|
+
writeStore,
|
|
738
|
+
});
|
|
706
739
|
console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
|
|
707
740
|
continue;
|
|
708
741
|
}
|
package/dist/store.d.ts
CHANGED
package/dist/store.js
CHANGED
|
@@ -16,6 +16,9 @@ export function parseStore(raw) {
|
|
|
16
16
|
const data = raw ? JSON.parse(raw) : { accounts: {} };
|
|
17
17
|
if (!data.accounts)
|
|
18
18
|
data.accounts = {};
|
|
19
|
+
if (typeof data.lastAccountSwitchAt !== "number" || Number.isNaN(data.lastAccountSwitchAt)) {
|
|
20
|
+
delete data.lastAccountSwitchAt;
|
|
21
|
+
}
|
|
19
22
|
if (data.loopSafetyEnabled !== false)
|
|
20
23
|
data.loopSafetyEnabled = true;
|
|
21
24
|
if (data.networkRetryEnabled !== true)
|