zames_pro 2.29.14 → 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 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
- const CONTINUE_NAMES = ['Continue', 'Продолжить', 'Продолжение'];
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 = 5000, 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
- for (const name of CONTINUE_NAMES) {
1146
- const loc = this.page.getByRole('button', { name }).first();
1147
- if ((await loc.count().catch(() => 0)) === 0)
1148
- continue;
1149
- if (await loc.isVisible().catch(() => false))
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|продолжить|продолжение)\s*[.!…]?$/i.test(t);
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
- for (const name of CONTINUE_NAMES) {
1196
- try {
1197
- const loc = this.page.getByRole('button', { name }).first();
1198
- if ((await loc.count().catch(() => 0)) === 0)
1199
- continue;
1200
- if (!(await loc.isVisible().catch(() => false)))
1201
- continue;
1202
- found = loc;
1203
- break;
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 effective send interval: a `chat/continue`
1209
- // request hits the rate limit like a send, and a run that keeps getting
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.sendIntervalMs() - (Date.now() - since);
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|продолжить|продолжение)\s*[.!…]?$/i.test(t);
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);
@@ -1654,12 +1678,12 @@ export class DeepSeekBrowser {
1654
1678
  // delayed by the remaining seconds, and the stop request only took effect
1655
1679
  // after the pause. This is the main "Esc does not cancel the pause" bug.
1656
1680
  // The effective minimum interval between two sends. When Deep thinking is on
1657
- // we add `thinkingExtraMs` (default +5s): reasoning turns add extra requests
1681
+ // we add `thinkingExtraMs` (default +2s): reasoning turns add extra requests
1658
1682
  // (`chat/continue` clicks, truncation retries), so a small margin reduces the
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
@@ -41,12 +41,13 @@ export const DEFAULTS = {
41
41
  stabilityChecks: 2,
42
42
  stabilityDelayMs: 400,
43
43
  minSendIntervalMs: 15000,
44
- thinkingExtraMs: 5000,
44
+ thinkingExtraMs: 2000,
45
45
  rateLimitWaitMs: 300000,
46
46
  maxRateLimitRetries: 6,
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
- try {
2372
- summary = await browser.ask(buildCompactPrompt(currentLocale), {
2373
- agent: true,
2374
- timeout: Math.max(60_000, config.browser.answerTimeoutMs),
2375
- });
2376
- }
2377
- catch (e) {
2378
- console.error(theme.error(t('compact.summary_failed', { v: e.message })));
2379
- continue;
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 = String(summary || '').trim();
2382
- // A model "answer" that is actually an error/abort sentinel is not a
2383
- // summary - do not carry it over.
2384
- if (!summary || /^\(прервано пользователем\)$/.test(summary)) {
2385
- console.error(theme.error(t('compact.summary_failed', {
2386
- v: summary || t('common.unknown'),
2387
- })));
2388
- continue;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.29.14",
3
+ "version": "2.29.16",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {