opencode-copilot-account-switcher 0.2.7 → 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 +366 -62
- package/dist/copilot-retry-notifier.d.ts +33 -0
- package/dist/copilot-retry-notifier.js +69 -0
- package/dist/loop-safety-plugin.d.ts +1 -1
- package/dist/loop-safety-plugin.js +4 -1
- 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 MAX_INPUT_ID_REPAIR_ATTEMPTS = 3;
|
|
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";
|
|
@@ -70,6 +69,9 @@ function parseInputIdTooLongDetails(text) {
|
|
|
70
69
|
function buildIdPreview(id) {
|
|
71
70
|
return `${id.slice(0, 12)}...`;
|
|
72
71
|
}
|
|
72
|
+
function buildMessagePreview(message) {
|
|
73
|
+
return message.slice(0, 160);
|
|
74
|
+
}
|
|
73
75
|
function getPayloadCandidates(payload) {
|
|
74
76
|
const input = payload.input;
|
|
75
77
|
if (!Array.isArray(input))
|
|
@@ -89,33 +91,82 @@ function hasLongInputIds(payload) {
|
|
|
89
91
|
return false;
|
|
90
92
|
return input.some((item) => typeof item?.id === "string" && (item.id?.length ?? 0) > 64);
|
|
91
93
|
}
|
|
92
|
-
function
|
|
94
|
+
function countLongInputIdCandidates(payload) {
|
|
95
|
+
const input = payload?.input;
|
|
96
|
+
if (!Array.isArray(input))
|
|
97
|
+
return 0;
|
|
98
|
+
return input.filter((item) => typeof item?.id === "string" && (item.id?.length ?? 0) > 64)
|
|
99
|
+
.length;
|
|
100
|
+
}
|
|
101
|
+
function collectLongInputIdCandidates(payload) {
|
|
93
102
|
const input = payload.input;
|
|
94
103
|
if (!Array.isArray(input))
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
return [];
|
|
105
|
+
return input.flatMap((item, payloadIndex) => {
|
|
106
|
+
const id = item?.id;
|
|
107
|
+
if (typeof id !== "string" || id.length <= 64)
|
|
108
|
+
return [];
|
|
109
|
+
return [{ item: item, payloadIndex, idLength: id.length }];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength) {
|
|
113
|
+
const matches = collectLongInputIdCandidates(payload);
|
|
114
|
+
if (matches.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
strategy: "ambiguous",
|
|
117
|
+
candidates: [],
|
|
118
|
+
reportedLengthMatched: reportedLength === undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
99
121
|
const lengthMatches = reportedLength
|
|
100
|
-
? matches.filter((item) =>
|
|
122
|
+
? matches.filter((item) => item.idLength === reportedLength)
|
|
101
123
|
: matches;
|
|
102
|
-
if (lengthMatches.length ===
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
147
|
+
const narrowedCandidates = lengthMatches.length > 0 ? lengthMatches : matches;
|
|
148
|
+
if (typeof serverReportedIndex === "number") {
|
|
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
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
strategy: "ambiguous",
|
|
161
|
+
candidates: narrowedCandidates,
|
|
162
|
+
reportedLengthMatched: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function stripTargetedLongInputId(payload, serverReportedIndex, reportedLength) {
|
|
115
166
|
const input = payload.input;
|
|
116
167
|
if (!Array.isArray(input))
|
|
117
168
|
return payload;
|
|
118
|
-
const target =
|
|
169
|
+
const target = getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength).candidate?.item;
|
|
119
170
|
if (!target)
|
|
120
171
|
return payload;
|
|
121
172
|
let changed = false;
|
|
@@ -159,6 +210,21 @@ function buildRetryInit(init, payload) {
|
|
|
159
210
|
body: JSON.stringify(payload),
|
|
160
211
|
};
|
|
161
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
|
+
}
|
|
162
228
|
function stripInternalSessionHeaderFromRequest(request) {
|
|
163
229
|
if (!(request instanceof Request))
|
|
164
230
|
return request;
|
|
@@ -177,13 +243,19 @@ function getHeader(request, init, name) {
|
|
|
177
243
|
return request.headers.get(name) ?? undefined;
|
|
178
244
|
return undefined;
|
|
179
245
|
}
|
|
180
|
-
function getTargetedInputId(payload, reportedLength) {
|
|
181
|
-
const target =
|
|
246
|
+
function getTargetedInputId(payload, serverReportedIndex, reportedLength) {
|
|
247
|
+
const target = getTargetedLongInputIdSelection(payload, serverReportedIndex, reportedLength).candidate?.item;
|
|
182
248
|
const id = target?.id;
|
|
183
249
|
if (typeof id !== "string")
|
|
184
250
|
return undefined;
|
|
185
251
|
return id;
|
|
186
252
|
}
|
|
253
|
+
function logCleanupStopped(reason, details) {
|
|
254
|
+
debugLog("input-id retry cleanup-stopped", {
|
|
255
|
+
reason,
|
|
256
|
+
...(details ?? {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
187
259
|
function stripOpenAIItemId(part) {
|
|
188
260
|
const metadata = part.metadata;
|
|
189
261
|
if (!metadata || typeof metadata !== "object")
|
|
@@ -247,30 +319,72 @@ async function repairSessionPart(sessionID, failingId, ctx) {
|
|
|
247
319
|
body: JSON.stringify(body),
|
|
248
320
|
};
|
|
249
321
|
if (ctx?.patchPart) {
|
|
250
|
-
|
|
322
|
+
try {
|
|
323
|
+
await ctx.patchPart({ url: url.href, init });
|
|
324
|
+
debugLog("input-id retry session repair", {
|
|
325
|
+
partID: match.partID,
|
|
326
|
+
messageID: match.messageID,
|
|
327
|
+
sessionID,
|
|
328
|
+
});
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
debugLog("input-id retry session repair failed", {
|
|
333
|
+
partID: match.partID,
|
|
334
|
+
messageID: match.messageID,
|
|
335
|
+
sessionID,
|
|
336
|
+
error: String(error instanceof Error ? error.message : error),
|
|
337
|
+
});
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(url, init);
|
|
251
343
|
debugLog("input-id retry session repair", {
|
|
252
344
|
partID: match.partID,
|
|
253
345
|
messageID: match.messageID,
|
|
254
346
|
sessionID,
|
|
347
|
+
ok: response.ok,
|
|
255
348
|
});
|
|
256
|
-
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
debugLog("input-id retry session repair failed", {
|
|
351
|
+
partID: match.partID,
|
|
352
|
+
messageID: match.messageID,
|
|
353
|
+
sessionID,
|
|
354
|
+
status: response.status,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return response.ok;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
debugLog("input-id retry session repair failed", {
|
|
361
|
+
partID: match.partID,
|
|
362
|
+
messageID: match.messageID,
|
|
363
|
+
sessionID,
|
|
364
|
+
error: String(error instanceof Error ? error.message : error),
|
|
365
|
+
});
|
|
366
|
+
return false;
|
|
257
367
|
}
|
|
258
|
-
const response = await fetch(url, init);
|
|
259
|
-
debugLog("input-id retry session repair", {
|
|
260
|
-
partID: match.partID,
|
|
261
|
-
messageID: match.messageID,
|
|
262
|
-
sessionID,
|
|
263
|
-
ok: response.ok,
|
|
264
|
-
});
|
|
265
|
-
return response.ok;
|
|
266
368
|
}
|
|
267
|
-
async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx, sessionID) {
|
|
268
|
-
if (response.status !== 400)
|
|
269
|
-
return {
|
|
270
|
-
|
|
369
|
+
async function maybeRetryInputIdTooLong(request, init, response, baseFetch, requestPayload, ctx, sessionID, startedNotified = false) {
|
|
370
|
+
if (response.status !== 400) {
|
|
371
|
+
return {
|
|
372
|
+
response,
|
|
373
|
+
retried: false,
|
|
374
|
+
nextInit: init,
|
|
375
|
+
nextPayload: requestPayload,
|
|
376
|
+
retryState: undefined,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
271
379
|
if (!requestPayload || !hasLongInputIds(requestPayload)) {
|
|
272
380
|
debugLog("skip input-id retry: request has no long ids");
|
|
273
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
response,
|
|
383
|
+
retried: false,
|
|
384
|
+
nextInit: init,
|
|
385
|
+
nextPayload: requestPayload,
|
|
386
|
+
retryState: undefined,
|
|
387
|
+
};
|
|
274
388
|
}
|
|
275
389
|
debugLog("input-id retry candidate", {
|
|
276
390
|
status: response.status,
|
|
@@ -282,7 +396,13 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
282
396
|
.catch(() => "");
|
|
283
397
|
if (!responseText) {
|
|
284
398
|
debugLog("skip input-id retry: empty response body");
|
|
285
|
-
return {
|
|
399
|
+
return {
|
|
400
|
+
response,
|
|
401
|
+
retried: false,
|
|
402
|
+
nextInit: init,
|
|
403
|
+
nextPayload: requestPayload,
|
|
404
|
+
retryState: undefined,
|
|
405
|
+
};
|
|
286
406
|
}
|
|
287
407
|
let parsed = parseInputIdTooLongDetails(responseText);
|
|
288
408
|
let matched = parsed.matched;
|
|
@@ -307,36 +427,81 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
307
427
|
serverReportedIndex: parsed.serverReportedIndex,
|
|
308
428
|
reportedLength: parsed.reportedLength,
|
|
309
429
|
});
|
|
310
|
-
if (!matched)
|
|
311
|
-
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
430
|
+
if (!matched) {
|
|
431
|
+
return {
|
|
432
|
+
response,
|
|
433
|
+
retried: false,
|
|
434
|
+
nextInit: init,
|
|
435
|
+
nextPayload: requestPayload,
|
|
436
|
+
retryState: undefined,
|
|
437
|
+
};
|
|
317
438
|
}
|
|
318
439
|
const payloadCandidates = getPayloadCandidates(requestPayload);
|
|
440
|
+
const targetSelection = getTargetedLongInputIdSelection(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
319
441
|
debugLog("input-id retry payload candidates", {
|
|
320
442
|
serverReportedIndex: parsed.serverReportedIndex,
|
|
321
443
|
candidates: payloadCandidates,
|
|
322
444
|
});
|
|
323
|
-
const failingId = getTargetedInputId(requestPayload, parsed.reportedLength);
|
|
324
|
-
const targetedPayload =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
445
|
+
const failingId = getTargetedInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
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,
|
|
331
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
|
+
};
|
|
332
481
|
}
|
|
482
|
+
const notifiedStarted = startedNotified || remainingBefore > 0;
|
|
483
|
+
let repairFailed = false;
|
|
333
484
|
if (sessionID && failingId) {
|
|
334
|
-
await repairSessionPart(sessionID, failingId, ctx).catch(() => false);
|
|
485
|
+
repairFailed = !(await repairSessionPart(sessionID, failingId, ctx).catch(() => false));
|
|
335
486
|
}
|
|
336
|
-
const sanitized = stripTargetedLongInputId(requestPayload, parsed.reportedLength);
|
|
487
|
+
const sanitized = stripTargetedLongInputId(requestPayload, parsed.serverReportedIndex, parsed.reportedLength);
|
|
337
488
|
if (sanitized === requestPayload) {
|
|
338
489
|
debugLog("skip input-id retry: sanitize made no changes");
|
|
339
|
-
return {
|
|
490
|
+
return {
|
|
491
|
+
response,
|
|
492
|
+
retried: false,
|
|
493
|
+
nextInit: init,
|
|
494
|
+
nextPayload: requestPayload,
|
|
495
|
+
retryState: {
|
|
496
|
+
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
497
|
+
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
498
|
+
remainingLongIdCandidatesBefore: remainingBefore,
|
|
499
|
+
remainingLongIdCandidatesAfter: remainingBefore,
|
|
500
|
+
previousReportedLength: parsed.reportedLength,
|
|
501
|
+
notifiedStarted,
|
|
502
|
+
repairFailed,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
340
505
|
}
|
|
341
506
|
debugLog("input-id retry triggered", {
|
|
342
507
|
removedLongIds: true,
|
|
@@ -344,11 +509,20 @@ async function maybeRetryInputIdTooLong(request, init, response, baseFetch, ctx,
|
|
|
344
509
|
});
|
|
345
510
|
const nextInit = buildRetryInit(init, sanitized);
|
|
346
511
|
const retried = await baseFetch(request, nextInit);
|
|
512
|
+
const retryState = {
|
|
513
|
+
previousServerReportedIndex: parsed.serverReportedIndex,
|
|
514
|
+
previousErrorMessagePreview: buildMessagePreview(responseText),
|
|
515
|
+
remainingLongIdCandidatesBefore: remainingBefore,
|
|
516
|
+
remainingLongIdCandidatesAfter: countLongInputIdCandidates(parseJsonBody(nextInit)),
|
|
517
|
+
previousReportedLength: parsed.reportedLength,
|
|
518
|
+
notifiedStarted,
|
|
519
|
+
repairFailed,
|
|
520
|
+
};
|
|
347
521
|
debugLog("input-id retry response", {
|
|
348
522
|
status: retried.status,
|
|
349
523
|
contentType: retried.headers.get("content-type") ?? undefined,
|
|
350
524
|
});
|
|
351
|
-
return { response: retried, retried: true, nextInit };
|
|
525
|
+
return { response: retried, retried: true, nextInit, nextPayload: sanitized, retryState };
|
|
352
526
|
}
|
|
353
527
|
function toRetryableSystemError(error) {
|
|
354
528
|
const base = error instanceof Error ? error : new Error(String(error));
|
|
@@ -370,6 +544,57 @@ function isCopilotUrl(request) {
|
|
|
370
544
|
return false;
|
|
371
545
|
}
|
|
372
546
|
}
|
|
547
|
+
async function getInputIdRetryErrorDetails(response) {
|
|
548
|
+
if (response.status !== 400)
|
|
549
|
+
return undefined;
|
|
550
|
+
const responseText = await response
|
|
551
|
+
.clone()
|
|
552
|
+
.text()
|
|
553
|
+
.catch(() => "");
|
|
554
|
+
if (!responseText)
|
|
555
|
+
return undefined;
|
|
556
|
+
let parsed = parseInputIdTooLongDetails(responseText);
|
|
557
|
+
let matched = parsed.matched;
|
|
558
|
+
let message = responseText;
|
|
559
|
+
if (!matched) {
|
|
560
|
+
try {
|
|
561
|
+
const bodyPayload = JSON.parse(responseText);
|
|
562
|
+
const error = bodyPayload.error;
|
|
563
|
+
message = String(error?.message ?? "");
|
|
564
|
+
parsed = parseInputIdTooLongDetails(message);
|
|
565
|
+
matched = parsed.matched || isInputIdTooLongErrorBody(bodyPayload);
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
matched = false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!matched)
|
|
572
|
+
return undefined;
|
|
573
|
+
return {
|
|
574
|
+
serverReportedIndex: parsed.serverReportedIndex,
|
|
575
|
+
reportedLength: parsed.reportedLength,
|
|
576
|
+
errorMessagePreview: buildMessagePreview(message),
|
|
577
|
+
};
|
|
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
|
+
}
|
|
373
598
|
function withStreamDebugLogs(response, request) {
|
|
374
599
|
if (!isDebugEnabled())
|
|
375
600
|
return response;
|
|
@@ -418,7 +643,7 @@ export function isRetryableCopilotFetchError(error) {
|
|
|
418
643
|
return RETRYABLE_MESSAGES.some((part) => message.includes(part));
|
|
419
644
|
}
|
|
420
645
|
export function createCopilotRetryingFetch(baseFetch, options) {
|
|
421
|
-
|
|
646
|
+
const notifier = options?.notifier ?? noopNotifier;
|
|
422
647
|
return async function retryingFetch(request, init) {
|
|
423
648
|
const sessionID = getHeader(request, init, INTERNAL_SESSION_HEADER);
|
|
424
649
|
const safeRequest = stripInternalSessionHeaderFromRequest(request);
|
|
@@ -434,6 +659,7 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
434
659
|
url: safeRequest instanceof Request ? safeRequest.url : safeRequest instanceof URL ? safeRequest.href : String(safeRequest),
|
|
435
660
|
isCopilot: isCopilotUrl(safeRequest),
|
|
436
661
|
});
|
|
662
|
+
let currentPayload = await parseJsonRequestPayload(safeRequest, effectiveInit);
|
|
437
663
|
try {
|
|
438
664
|
const response = await baseFetch(safeRequest, effectiveInit);
|
|
439
665
|
debugLog("fetch resolved", {
|
|
@@ -443,12 +669,90 @@ export function createCopilotRetryingFetch(baseFetch, options) {
|
|
|
443
669
|
if (isCopilotUrl(safeRequest)) {
|
|
444
670
|
let currentResponse = response;
|
|
445
671
|
let currentInit = effectiveInit;
|
|
446
|
-
|
|
447
|
-
|
|
672
|
+
let attempts = 0;
|
|
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);
|
|
448
680
|
currentResponse = result.response;
|
|
449
681
|
currentInit = result.nextInit;
|
|
450
|
-
|
|
682
|
+
currentPayload = result.nextPayload;
|
|
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
|
+
}
|
|
692
|
+
const currentError = await getInputIdRetryErrorDetails(currentResponse);
|
|
693
|
+
let stopReason = result.retryState.stopReason;
|
|
694
|
+
const madeProgress = result.retryState.remainingLongIdCandidatesAfter < result.retryState.remainingLongIdCandidatesBefore;
|
|
695
|
+
if (!stopReason && result.retryState.remainingLongIdCandidatesAfter >= result.retryState.remainingLongIdCandidatesBefore) {
|
|
696
|
+
stopReason = "remaining-candidates-not-reduced";
|
|
697
|
+
}
|
|
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";
|
|
704
|
+
}
|
|
705
|
+
if (!stopReason && currentError && result.retryState.remainingLongIdCandidatesAfter === 0) {
|
|
706
|
+
stopReason = "local-candidates-exhausted";
|
|
707
|
+
}
|
|
708
|
+
if ((currentError || stopReason) && stopReason !== "evidence-insufficient") {
|
|
709
|
+
debugLog("input-id retry progress", {
|
|
710
|
+
attempt: attempts + 1,
|
|
711
|
+
previousServerReportedIndex: result.retryState.previousServerReportedIndex,
|
|
712
|
+
currentServerReportedIndex: currentError?.serverReportedIndex,
|
|
713
|
+
serverIndexChanged: result.retryState.previousServerReportedIndex !== currentError?.serverReportedIndex,
|
|
714
|
+
previousErrorMessagePreview: result.retryState.previousErrorMessagePreview,
|
|
715
|
+
currentErrorMessagePreview: currentError?.errorMessagePreview,
|
|
716
|
+
remainingLongIdCandidatesBefore: result.retryState.remainingLongIdCandidatesBefore,
|
|
717
|
+
remainingLongIdCandidatesAfter: result.retryState.remainingLongIdCandidatesAfter,
|
|
718
|
+
stopReason,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
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;
|
|
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
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (!result.retried) {
|
|
749
|
+
if (startedNotified && !finishedNotified) {
|
|
750
|
+
await notify(notifier, "stopped", countLongInputIdCandidates(currentPayload));
|
|
751
|
+
finishedNotified = true;
|
|
752
|
+
}
|
|
451
753
|
break;
|
|
754
|
+
}
|
|
755
|
+
attempts += 1;
|
|
452
756
|
}
|
|
453
757
|
return withStreamDebugLogs(currentResponse, safeRequest);
|
|
454
758
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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- 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- 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.
|
|
@@ -26,6 +27,8 @@ export const LOOP_SAFETY_POLICY = `Guided Loop Safety Policy
|
|
|
26
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.
|
|
27
28
|
- MANDATORY: Never send assistant text as a post-report fallback in either branch.
|
|
28
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.
|
|
29
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.
|
|
30
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.
|
|
31
34
|
- Dispatching task or subagent work is expensive and should be avoided unless it materially improves the result.
|
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)
|