zames_pro 2.29.15 → 2.29.16
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/browser.js +47 -24
- package/dist/commands.js +37 -2
- package/dist/config.js +9 -0
- package/dist/i18n.js +12 -0
- package/dist/index.js +93 -18
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -170,7 +170,17 @@ const STOP_SELECTORS = [
|
|
|
170
170
|
// The button is found by Playwright's accessible name (getByRole) with an
|
|
171
171
|
// EXACT match, falling back to a raw DOM scan. The class names change between
|
|
172
172
|
// builds, so we do not rely on them.
|
|
173
|
-
|
|
173
|
+
// Reasoning (Deep thinking) mode uses a LONGER label: the server pauses the
|
|
174
|
+
// THINK phase and offers «Продолжить размышление» / «Continue thinking», NOT a
|
|
175
|
+
// bare «Continue». The old exact list missed those, so with thinking ON the
|
|
176
|
+
// button was never found — neither for the auto-click NOR for the "is the turn
|
|
177
|
+
// paused?" guard — and the operator had to press it by hand.
|
|
178
|
+
//
|
|
179
|
+
// We match the accessible name with a REGEX: a bare continue word OR the same
|
|
180
|
+
// word followed by a short reasoning/answer suffix. It is anchored and
|
|
181
|
+
// length-limited, so a "Continue" inside rendered prose is never clicked (the
|
|
182
|
+
// old non-exact `getByRole` matched any button CONTAINING the word).
|
|
183
|
+
export const CONTINUE_NAME_RE = /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i;
|
|
174
184
|
// DeepSeek UI service statuses that are NOT the model's answer.
|
|
175
185
|
// Otherwise the agent takes a status (Reading...) for an answer and breaks parsing.
|
|
176
186
|
const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
|
|
@@ -297,6 +307,9 @@ export class DeepSeekBrowser {
|
|
|
297
307
|
// Click DeepSeek's "Continue" button automatically when the reasoning phase
|
|
298
308
|
// is paused by the server (deep thinking mode).
|
|
299
309
|
autoContinue;
|
|
310
|
+
// Min gap between Continue clicks in thinking mode (ms). Smaller than
|
|
311
|
+
// minSendIntervalMs so the resume does not feel sluggish.
|
|
312
|
+
continueMinGapMs;
|
|
300
313
|
// Desired state of the DeepSeek chat toggles, applied before each send.
|
|
301
314
|
// deepThinking: the "Deep thinking" toggle (reasoning; the reasoning text
|
|
302
315
|
// is never read/shown). webSearch: the "Smart search" toggle.
|
|
@@ -378,7 +391,7 @@ export class DeepSeekBrowser {
|
|
|
378
391
|
// must be stripped (DeepSeek's CDN answers 403 to it); computed in the
|
|
379
392
|
// constructor, overridable by an explicit UA in the options/config.
|
|
380
393
|
userAgent;
|
|
381
|
-
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
|
|
394
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
|
|
382
395
|
this.headless = headless;
|
|
383
396
|
this.debug = debug;
|
|
384
397
|
this.channel = channel;
|
|
@@ -395,6 +408,7 @@ export class DeepSeekBrowser {
|
|
|
395
408
|
this.maxIncompleteRetries = maxIncompleteRetries;
|
|
396
409
|
this.incompleteWaitMs = incompleteWaitMs;
|
|
397
410
|
this.autoContinue = autoContinue;
|
|
411
|
+
this.continueMinGapMs = continueMinGapMs;
|
|
398
412
|
this.deepThinking = deepThinking;
|
|
399
413
|
this.webSearch = webSearch;
|
|
400
414
|
this.auth = {
|
|
@@ -1142,11 +1156,13 @@ export class DeepSeekBrowser {
|
|
|
1142
1156
|
// text there is exactly the "agent stopped with a Continue button" bug.
|
|
1143
1157
|
async _continueButtonVisible() {
|
|
1144
1158
|
try {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1159
|
+
const loc = this.page.getByRole('button', { name: CONTINUE_NAME_RE });
|
|
1160
|
+
const n = await loc.count().catch(() => 0);
|
|
1161
|
+
for (let i = 0; i < n; i++) {
|
|
1162
|
+
if (await loc
|
|
1163
|
+
.nth(i)
|
|
1164
|
+
.isVisible()
|
|
1165
|
+
.catch(() => false))
|
|
1150
1166
|
return true;
|
|
1151
1167
|
}
|
|
1152
1168
|
}
|
|
@@ -1158,7 +1174,7 @@ export class DeepSeekBrowser {
|
|
|
1158
1174
|
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
1159
1175
|
.replace(/\s+/g, ' ')
|
|
1160
1176
|
.trim();
|
|
1161
|
-
const isExact = (t) => /^(continue|продолжить|продолжение)
|
|
1177
|
+
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
1162
1178
|
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1163
1179
|
for (const e of cands) {
|
|
1164
1180
|
if (!isExact(labelOf(e)))
|
|
@@ -1192,26 +1208,34 @@ export class DeepSeekBrowser {
|
|
|
1192
1208
|
// probing this every tick must stay cheap, and there is no point
|
|
1193
1209
|
// throttling a click that will not happen.
|
|
1194
1210
|
let found = null;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1211
|
+
try {
|
|
1212
|
+
const loc = this.page.getByRole('button', { name: CONTINUE_NAME_RE });
|
|
1213
|
+
const n = await loc.count().catch(() => 0);
|
|
1214
|
+
for (let i = 0; i < n; i++) {
|
|
1215
|
+
if (await loc
|
|
1216
|
+
.nth(i)
|
|
1217
|
+
.isVisible()
|
|
1218
|
+
.catch(() => false)) {
|
|
1219
|
+
found = loc.nth(i);
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1204
1222
|
}
|
|
1205
|
-
catch { }
|
|
1206
1223
|
}
|
|
1224
|
+
catch { }
|
|
1207
1225
|
if (found) {
|
|
1208
|
-
// One Continue click per
|
|
1209
|
-
//
|
|
1226
|
+
// One Continue click per `continueMinGapMs`: a `chat/continue` request
|
|
1227
|
+
// hits the rate limit like a send, and a run that keeps getting
|
|
1210
1228
|
// truncated could otherwise fire clicks back-to-back. The gap counts
|
|
1211
1229
|
// from the LATEST of the last send and the last Continue click.
|
|
1230
|
+
//
|
|
1231
|
+
// NOTE: the gap is DELIBERATELY smaller than `sendIntervalMs` (15s): the
|
|
1232
|
+
// reasoning pause shows the button immediately, and waiting out the full
|
|
1233
|
+
// send interval made every resume feel sluggish (the operator saw a
|
|
1234
|
+
// frozen turn for ~15s before the click). 1.5s is enough to avoid
|
|
1235
|
+
// hammering `chat/continue` while resuming almost immediately.
|
|
1212
1236
|
const since = Math.max(this._lastSentAt, this._lastContinueAt);
|
|
1213
1237
|
if (since) {
|
|
1214
|
-
const gap = this.
|
|
1238
|
+
const gap = this.continueMinGapMs - (Date.now() - since);
|
|
1215
1239
|
if (gap > 0) {
|
|
1216
1240
|
const aborted = await this._sleepInterruptible(gap, (leftMs) => {
|
|
1217
1241
|
if (this.onSendPause) {
|
|
@@ -1239,7 +1263,7 @@ export class DeepSeekBrowser {
|
|
|
1239
1263
|
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
1240
1264
|
.replace(/\s+/g, ' ')
|
|
1241
1265
|
.trim();
|
|
1242
|
-
const isExact = (t) => /^(continue|продолжить|продолжение)
|
|
1266
|
+
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
1243
1267
|
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1244
1268
|
for (const e of cands) {
|
|
1245
1269
|
const t = labelOf(e);
|
|
@@ -1659,7 +1683,7 @@ export class DeepSeekBrowser {
|
|
|
1659
1683
|
// chance of hitting the rate limit. `deepThinking` is the DESIRED state read
|
|
1660
1684
|
// from the config (set in the constructor / on /config change).
|
|
1661
1685
|
sendIntervalMs() {
|
|
1662
|
-
return this.minSendIntervalMs + (this.deepThinking ? this.thinkingExtraMs : 0);
|
|
1686
|
+
return (this.minSendIntervalMs + (this.deepThinking ? this.thinkingExtraMs : 0));
|
|
1663
1687
|
}
|
|
1664
1688
|
async _waitForSendSlot(agent) {
|
|
1665
1689
|
if (!agent)
|
|
@@ -1951,7 +1975,6 @@ export class DeepSeekBrowser {
|
|
|
1951
1975
|
await this.page.waitForTimeout(500);
|
|
1952
1976
|
started = true;
|
|
1953
1977
|
// fall through to the finish loop by NOT returning here
|
|
1954
|
-
lastStartCur = cur;
|
|
1955
1978
|
}
|
|
1956
1979
|
else {
|
|
1957
1980
|
throw new GenerationIncompleteError(this._netBody.slice(-300));
|
package/dist/commands.js
CHANGED
|
@@ -342,7 +342,9 @@ export function buildCompactPrompt(locale = 'ru') {
|
|
|
342
342
|
'(3) the exact current state (files changed, commands run, their results); ' +
|
|
343
343
|
'(4) open questions and the next concrete steps. ' +
|
|
344
344
|
'Keep file paths, function/identifier names, commands and error texts verbatim. ' +
|
|
345
|
-
'Be concise but complete — no small talk, no code dumps beyond short essential snippets.'
|
|
345
|
+
'Be concise but complete — no small talk, no code dumps beyond short essential snippets. ' +
|
|
346
|
+
'Reply with the summary text ONLY. Do NOT call any tools and do NOT output tool-call ' +
|
|
347
|
+
'JSON or DSML — this is a handover summary, not a continuation of the work.');
|
|
346
348
|
}
|
|
347
349
|
return ('Сожми историю диалога в краткое резюме для НОВОГО чата. ' +
|
|
348
350
|
'Это резюме будет ЕДИНСТВЕННЫМ контекстом, с которым новый чат начнёт работу, поэтому оно должно быть самодостаточным. ' +
|
|
@@ -350,7 +352,40 @@ export function buildCompactPrompt(locale = 'ru') {
|
|
|
350
352
|
'(3) точное текущее состояние (изменённые файлы, выполненные команды и их результаты); ' +
|
|
351
353
|
'(4) открытые вопросы и следующие конкретные шаги. ' +
|
|
352
354
|
'Пути к файлам, имена функций/идентификаторов, команды и тексты ошибок сохраняй дословно. ' +
|
|
353
|
-
'Пиши кратко, но полно — без воды и без больших дампов кода (только короткие важные фрагменты).'
|
|
355
|
+
'Пиши кратко, но полно — без воды и без больших дампов кода (только короткие важные фрагменты). ' +
|
|
356
|
+
'В ответе верни ТОЛЬКО текст резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова ' +
|
|
357
|
+
'инструмента — это резюме для передачи, а не продолжение работы.');
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Is the model's /compact answer a usable handover summary?
|
|
361
|
+
*
|
|
362
|
+
* The summary request is sent into the OLD chat as a normal turn, so in
|
|
363
|
+
* reasoning mode the model can answer it with a TOOL CALL instead of prose
|
|
364
|
+
* (observed live: it emitted a Bash call to "gather the current state" and
|
|
365
|
+
* never produced a summary — only a THINK fragment). A tool-call JSON is
|
|
366
|
+
* protocol noise and must NOT be carried into the new chat, otherwise the new
|
|
367
|
+
* chat starts by re-running an old command. An empty answer / the abort
|
|
368
|
+
* sentinel is unusable too.
|
|
369
|
+
*/
|
|
370
|
+
export function isUsableCompactSummary(text) {
|
|
371
|
+
const t = String(text || '').trim();
|
|
372
|
+
if (!t)
|
|
373
|
+
return false;
|
|
374
|
+
if (t.length < 40)
|
|
375
|
+
return false;
|
|
376
|
+
if (/^\(прервано пользователем\)$/.test(t))
|
|
377
|
+
return false;
|
|
378
|
+
if (parseAssistantToolCall(t))
|
|
379
|
+
return false;
|
|
380
|
+
if (/DSML/i.test(t))
|
|
381
|
+
return false;
|
|
382
|
+
// A tool-call body without a recognizable head (a cut-off stream) still
|
|
383
|
+
// carries "args": { ... }.
|
|
384
|
+
if (/"args"\s*:\s*\{/.test(t))
|
|
385
|
+
return false;
|
|
386
|
+
if (/<ds_safety>/i.test(t))
|
|
387
|
+
return false;
|
|
388
|
+
return true;
|
|
354
389
|
}
|
|
355
390
|
/**
|
|
356
391
|
* Wrap the model's summary into the text posted as the first message of the
|
package/dist/config.js
CHANGED
|
@@ -47,6 +47,7 @@ export const DEFAULTS = {
|
|
|
47
47
|
maxIncompleteRetries: 4,
|
|
48
48
|
incompleteWaitMs: 2000,
|
|
49
49
|
autoContinue: true,
|
|
50
|
+
continueMinGapMs: 1500,
|
|
50
51
|
resendPromptOnResume: false,
|
|
51
52
|
deepThinking: false,
|
|
52
53
|
webSearch: true,
|
|
@@ -265,6 +266,14 @@ export const CONFIG_SCHEMA = [
|
|
|
265
266
|
labelKey: 'cfg.f.browser_autoContinue',
|
|
266
267
|
groupKey: 'cfg.group.browser',
|
|
267
268
|
},
|
|
269
|
+
{
|
|
270
|
+
path: 'browser.continueMinGapMs',
|
|
271
|
+
type: 'number',
|
|
272
|
+
min: 0,
|
|
273
|
+
max: 600000,
|
|
274
|
+
labelKey: 'cfg.f.browser_continueMinGapMs',
|
|
275
|
+
groupKey: 'cfg.group.browser',
|
|
276
|
+
},
|
|
268
277
|
{
|
|
269
278
|
path: 'browser.resendPromptOnResume',
|
|
270
279
|
type: 'boolean',
|
package/dist/i18n.js
CHANGED
|
@@ -213,6 +213,14 @@ const CATALOG = {
|
|
|
213
213
|
ru: 'Не удалось получить резюме от модели: {v}',
|
|
214
214
|
en: 'Could not get the summary from the model: {v}',
|
|
215
215
|
},
|
|
216
|
+
'compact.summary_retry': {
|
|
217
|
+
ru: 'Не удалось получить резюме ({v}). Повтор {attempt}/{max}...',
|
|
218
|
+
en: 'Could not get the summary ({v}). Retry {attempt}/{max}...',
|
|
219
|
+
},
|
|
220
|
+
'compact.summary_fallback': {
|
|
221
|
+
ru: 'Модель не смогла сделать резюме — переношу последние {v} сообщений диалога как есть.',
|
|
222
|
+
en: 'The model could not produce a summary — carrying the last {v} messages over as-is.',
|
|
223
|
+
},
|
|
216
224
|
'compact.done': {
|
|
217
225
|
ru: '✅ История сжата, открыт новый чат с резюме.',
|
|
218
226
|
en: '✅ History compacted, a new chat with the summary is open.',
|
|
@@ -898,6 +906,10 @@ const CATALOG = {
|
|
|
898
906
|
ru: 'Автонажатие кнопки Continue (режим размышления)',
|
|
899
907
|
en: 'Auto-click Continue (reasoning mode)',
|
|
900
908
|
},
|
|
909
|
+
'cfg.f.browser_continueMinGapMs': {
|
|
910
|
+
ru: 'Мин. пауза перед нажатием Continue, мс',
|
|
911
|
+
en: 'Min gap before a Continue click, ms',
|
|
912
|
+
},
|
|
901
913
|
'cfg.f.browser_resendPromptOnResume': {
|
|
902
914
|
ru: 'Пересылать системный промпт при возобновлении чата',
|
|
903
915
|
en: 'Resend the system prompt on chat resume',
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
16
16
|
import { Transcript } from './transcript.js';
|
|
17
17
|
import { UndoStore } from './undo.js';
|
|
18
18
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
19
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, buildCompactPrompt, buildCompactCarryover, } from './commands.js';
|
|
19
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, buildCompactPrompt, buildCompactCarryover, formatRestoredHistory, isUsableCompactSummary, } from './commands.js';
|
|
20
20
|
import { renderMarkdown } from './markdown.js';
|
|
21
21
|
import { closeWeb } from './web.js';
|
|
22
22
|
import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
|
|
@@ -55,6 +55,14 @@ function getPositional() {
|
|
|
55
55
|
}
|
|
56
56
|
return positional;
|
|
57
57
|
}
|
|
58
|
+
// /compact asks the OLD chat for a summary. If browser.ask() exhausts its own
|
|
59
|
+
// rate-limit budget, aborting the whole compaction used to force the operator
|
|
60
|
+
// to start over. We retry the summary call a few times instead.
|
|
61
|
+
const COMPACT_SUMMARY_ATTEMPTS = 3;
|
|
62
|
+
// How many restored messages the LOCAL fallback summary keeps when the model
|
|
63
|
+
// could not produce one (a rate limit / server error must not lose the
|
|
64
|
+
// compaction).
|
|
65
|
+
const COMPACT_FALLBACK_LIMIT = 40;
|
|
58
66
|
const config = loadConfig();
|
|
59
67
|
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
60
68
|
let currentLocale = isLocale(config.ui?.locale)
|
|
@@ -2367,25 +2375,92 @@ async function main() {
|
|
|
2367
2375
|
console.log(theme.system(t('compact.start')));
|
|
2368
2376
|
// 1) Ask the OLD chat to summarize itself. agent: true - this is a
|
|
2369
2377
|
// real back-and-forth, so the send throttle applies.
|
|
2378
|
+
//
|
|
2379
|
+
// The call is retried a few times: browser.ask() already retries the
|
|
2380
|
+
// "Messages too frequent" rate limit internally, but if it exhausts
|
|
2381
|
+
// its own budget the whole /compact used to abort and the operator
|
|
2382
|
+
// had to start over. A transient server error must NOT lose the
|
|
2383
|
+
// compaction, so we keep trying with a growing pause before giving up.
|
|
2370
2384
|
let summary = '';
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2385
|
+
let summaryErr = '';
|
|
2386
|
+
for (let attempt = 1; attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
2387
|
+
// On a retry after an unusable answer we append an explicit
|
|
2388
|
+
// "plain text only" note. In reasoning mode DeepSeek answered the
|
|
2389
|
+
// /compact prompt with a Bash TOOL CALL ("gather the current state")
|
|
2390
|
+
// instead of a summary — protocol noise that must NOT be carried
|
|
2391
|
+
// into the new chat.
|
|
2392
|
+
let prompt = buildCompactPrompt(currentLocale);
|
|
2393
|
+
if (attempt > 1) {
|
|
2394
|
+
prompt +=
|
|
2395
|
+
currentLocale === 'en'
|
|
2396
|
+
? '\n\nIMPORTANT: reply with the summary as PLAIN TEXT only. Do NOT call any tools and do NOT output tool-call JSON/DSML.'
|
|
2397
|
+
: '\n\nВАЖНО: ответь ТОЛЬКО текстом резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова инструмента.';
|
|
2398
|
+
}
|
|
2399
|
+
let answer = '';
|
|
2400
|
+
try {
|
|
2401
|
+
answer = await browser.ask(prompt, {
|
|
2402
|
+
agent: true,
|
|
2403
|
+
timeout: Math.max(60_000, config.browser.answerTimeoutMs),
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
catch (e) {
|
|
2407
|
+
summaryErr = e.message;
|
|
2408
|
+
}
|
|
2409
|
+
const trimmed = String(answer || '').trim();
|
|
2410
|
+
// The user aborted (Esc) — do not keep retrying.
|
|
2411
|
+
if (/^\(прервано пользователем\)$/.test(trimmed)) {
|
|
2412
|
+
summaryErr = trimmed;
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
if (!summaryErr && isUsableCompactSummary(trimmed)) {
|
|
2416
|
+
summary = trimmed;
|
|
2417
|
+
summaryErr = '';
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
// The turn may have succeeded yet produced an unusable answer (a
|
|
2421
|
+
// tool call, empty text). Record it and retry with the note.
|
|
2422
|
+
if (!summaryErr)
|
|
2423
|
+
summaryErr = trimmed || t('common.unknown');
|
|
2424
|
+
if (attempt < COMPACT_SUMMARY_ATTEMPTS) {
|
|
2425
|
+
console.error(theme.warn(t('compact.summary_retry', {
|
|
2426
|
+
v: summaryErr,
|
|
2427
|
+
attempt: String(attempt),
|
|
2428
|
+
max: String(COMPACT_SUMMARY_ATTEMPTS),
|
|
2429
|
+
})));
|
|
2430
|
+
await new Promise((r) => setTimeout(r, 3000 * attempt));
|
|
2431
|
+
}
|
|
2380
2432
|
}
|
|
2381
|
-
summary
|
|
2382
|
-
//
|
|
2383
|
-
// summary
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2433
|
+
// A model summary that never arrived (a server error, or only
|
|
2434
|
+
// tool-call answers) must NOT lose the compaction: fall back to a
|
|
2435
|
+
// LOCAL summary built from the restored chat history (the same
|
|
2436
|
+
// displayable dialogue /resume prints).
|
|
2437
|
+
if (!summary) {
|
|
2438
|
+
let fallback = '';
|
|
2439
|
+
try {
|
|
2440
|
+
const all = currentChatId && browser.fetchChatMessages
|
|
2441
|
+
? await browser.fetchChatMessages(currentChatId).catch(() => [])
|
|
2442
|
+
: [];
|
|
2443
|
+
const msgs = trimRestoredMessages(all, 0);
|
|
2444
|
+
if (msgs.length) {
|
|
2445
|
+
fallback = formatRestoredHistory(msgs, {
|
|
2446
|
+
limit: COMPACT_FALLBACK_LIMIT,
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
catch { }
|
|
2451
|
+
fallback = String(fallback || '').trim();
|
|
2452
|
+
if (fallback) {
|
|
2453
|
+
summary = fallback;
|
|
2454
|
+
console.error(theme.warn(t('compact.summary_fallback', {
|
|
2455
|
+
v: String(COMPACT_FALLBACK_LIMIT),
|
|
2456
|
+
})));
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
console.error(theme.error(t('compact.summary_failed', {
|
|
2460
|
+
v: summaryErr || t('common.unknown'),
|
|
2461
|
+
})));
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2389
2464
|
}
|
|
2390
2465
|
transcript.log('compact_summary', {
|
|
2391
2466
|
chars: summary.length,
|