zames_pro 2.1.2 → 2.2.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.
@@ -53,6 +53,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
53
53
  // её переотправить вызов. Ограничиваем число таких попыток.
54
54
  let malformedRetries = 0;
55
55
  const MAX_MALFORMED_RETRIES = 3;
56
+ // Счётчик «пустой/служебный ответ без вызова инструмента». Модель иногда
57
+ // отвечает статусом интерфейса (Reading…), тостом лимита или пустой
58
+ // строкой. Это НЕ финальный ответ — просим её продолжить, а не молча
59
+ // останавливаемся (раньше агент на этом вставал).
60
+ let stallRetries = 0;
61
+ const MAX_STALL_RETRIES = 5;
56
62
  for (let i = 0; i < maxIterations; i++) {
57
63
  onThinking();
58
64
  const rawResponse = await browser.ask(message);
@@ -98,6 +104,33 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
98
104
  'Например: {"tool": "Read", "args": {"path": "src/index.js"}}';
99
105
  continue;
100
106
  }
107
+ // Пустой или служебный ответ (статус интерфейса, тост лимита, пробелы)
108
+ // не считаем финальным: просим модель продолжить. Иначе агент
109
+ // останавливается, хотя должен был вызвать инструмент.
110
+ const trimmed = (rawResponse || '').trim();
111
+ const looksService = !trimmed ||
112
+ trimmed.length < 2 ||
113
+ /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
114
+ /(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed);
115
+ if (looksService && stallRetries < MAX_STALL_RETRIES) {
116
+ stallRetries++;
117
+ transcript?.log('stall_retry', {
118
+ attempt: stallRetries,
119
+ response: rawResponse,
120
+ });
121
+ if (debugLog) {
122
+ console.error('внимание: пустой/служебный ответ, прошу продолжить (попытка ' +
123
+ stallRetries +
124
+ '/' +
125
+ MAX_STALL_RETRIES +
126
+ ')');
127
+ }
128
+ message =
129
+ 'Продолжи выполнение задачи. Если нужен инструмент — ответь РОВНО ' +
130
+ 'одним JSON-объектом вызова: {"tool": "...", "args": {...}}. ' +
131
+ 'Если задача выполнена — вызови инструмент respond с итоговым сообщением.';
132
+ continue;
133
+ }
101
134
  onAssistantMessage(rawResponse);
102
135
  transcript?.log('assistant_final', { message: rawResponse });
103
136
  return rawResponse;
package/dist/browser.js CHANGED
@@ -39,6 +39,20 @@ const STOP_SELECTORS = [
39
39
  // Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
40
40
  // Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
41
41
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
42
+ // Ответ DeepSeek при превышении лимита частоты. В этом случае сообщение НЕ
43
+ // отправлено — нужно подождать и повторить, а не считать это ответом модели.
44
+ const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
45
+ export function isRateLimitText(text) {
46
+ return RATE_LIMIT_RE.test(String(text || ''));
47
+ }
48
+ // Ошибка «слишком часто»: отличается от прочих, чтобы ask() ждал долго
49
+ // (лимиты DeepSeek сбрасываются за минуты) и повторял отправку сам.
50
+ export class RateLimitError extends Error {
51
+ constructor(detail) {
52
+ super('Messages too frequent. Try again later. ' + detail);
53
+ this.name = 'RateLimitError';
54
+ }
55
+ }
42
56
  // ---------- profile cleanup ----------
43
57
  async function killStaleChrome() {
44
58
  if (process.platform !== 'win32')
@@ -83,6 +97,8 @@ export class DeepSeekBrowser {
83
97
  stabilityChecks;
84
98
  stabilityDelayMs;
85
99
  minSendIntervalMs;
100
+ rateLimitWaitMs;
101
+ maxRateLimitRetries;
86
102
  _lastSentAt;
87
103
  _abort;
88
104
  context;
@@ -93,7 +109,7 @@ export class DeepSeekBrowser {
93
109
  _netSniff;
94
110
  _netSniffLimit;
95
111
  _netHookInstalled;
96
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, } = {}) {
112
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
97
113
  this.headless = headless;
98
114
  this.debug = debug;
99
115
  this.channel = channel;
@@ -102,6 +118,8 @@ export class DeepSeekBrowser {
102
118
  this.stabilityChecks = stabilityChecks;
103
119
  this.stabilityDelayMs = stabilityDelayMs;
104
120
  this.minSendIntervalMs = minSendIntervalMs;
121
+ this.rateLimitWaitMs = rateLimitWaitMs;
122
+ this.maxRateLimitRetries = maxRateLimitRetries;
105
123
  this._lastSentAt = 0;
106
124
  this._abort = false;
107
125
  this._netCapture = '';
@@ -282,6 +300,13 @@ export class DeepSeekBrowser {
282
300
  return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
283
301
  }, ANSWER_SELECTORS);
284
302
  }
303
+ // Полный видимый текст страницы — по нему ловим тосты/ошибки интерфейса
304
+ // DeepSeek (в частности «Messages too frequent. Try again later.»).
305
+ async _readPageText() {
306
+ return await this.page
307
+ .evaluate(() => document.body.innerText || '')
308
+ .catch(() => '');
309
+ }
285
310
  async _readLastAnswerTextClean() {
286
311
  const raw = await this._readLastAnswerText().catch(() => '');
287
312
  const t = (raw || '').trim();
@@ -309,12 +334,30 @@ export class DeepSeekBrowser {
309
334
  }
310
335
  async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
311
336
  let lastErr = null;
312
- for (let attempt = 1; attempt <= this.askRetries; attempt++) {
337
+ let attempt = 0;
338
+ let rateLimitRetries = 0;
339
+ while (attempt < this.askRetries) {
340
+ attempt++;
313
341
  try {
314
342
  return await this._askOnce(prompt, { timeout });
315
343
  }
316
344
  catch (e) {
317
345
  lastErr = e;
346
+ // Лимит частоты: DeepSeek не принял сообщение. Ждём долго —
347
+ // лимиты сбрасываются за минуты, — и повторяем отправку сами.
348
+ // Эти паузы НЕ расходуют обычные попытки ask().
349
+ if (e instanceof RateLimitError) {
350
+ attempt--;
351
+ rateLimitRetries++;
352
+ if (rateLimitRetries > this.maxRateLimitRetries) {
353
+ console.error(theme.error(`✖ DeepSeek не принял сообщение после ${rateLimitRetries} пауз по ${Math.ceil(this.rateLimitWaitMs / 60000)} мин.`));
354
+ throw e;
355
+ }
356
+ console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
357
+ await this.page.waitForTimeout(this.rateLimitWaitMs);
358
+ await this.newChat().catch(() => { });
359
+ continue;
360
+ }
318
361
  console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
319
362
  if (/closed|crash|Target page|browser/i.test(e.message)) {
320
363
  console.error('⚠ перезапускаю браузер...');
@@ -416,13 +459,19 @@ export class DeepSeekBrowser {
416
459
  }
417
460
  this._lastSentAt = Date.now();
418
461
  // Ждём старта: либо появился Stop, либо изменился текст ответа,
419
- // либо вырос общий объём текста на странице.
462
+ // либо вырос общий объём текста на странице. Параллельно ловим
463
+ // тост о превышении лимита частоты — это НЕ ответ модели, а отказ
464
+ // приёма сообщения, и его надо обработать паузой и повтором.
420
465
  const startDeadline = Date.now() + 15_000;
421
466
  const startBodyLen = await this.page
422
467
  .evaluate(() => document.body.innerText.length)
423
468
  .catch(() => 0);
424
469
  let started = false;
425
470
  while (Date.now() < startDeadline) {
471
+ const pageText = await this._readPageText();
472
+ if (isRateLimitText(pageText)) {
473
+ throw new RateLimitError(pageText.slice(0, 300));
474
+ }
426
475
  const gen = await this._isGenerating();
427
476
  const cur = await this._readLastAnswerTextClean().catch(() => '');
428
477
  const bodyLen = await this.page
@@ -435,6 +484,12 @@ export class DeepSeekBrowser {
435
484
  await this.page.waitForTimeout(300);
436
485
  }
437
486
  if (!started) {
487
+ // Перед ошибкой ещё раз проверим на лимит частоты: иногда тост
488
+ // появляется с задержкой и не успевает попасть в ранние проверки.
489
+ const pageText = await this._readPageText();
490
+ if (isRateLimitText(pageText)) {
491
+ throw new RateLimitError(pageText.slice(0, 300));
492
+ }
438
493
  throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
439
494
  }
440
495
  // Ждём, пока ответ перестанет меняться. Условие "не генерируется"
package/dist/config.js CHANGED
@@ -38,6 +38,8 @@ export const DEFAULTS = {
38
38
  stabilityChecks: 3,
39
39
  stabilityDelayMs: 1000,
40
40
  minSendIntervalMs: 15000,
41
+ rateLimitWaitMs: 300000,
42
+ maxRateLimitRetries: 6,
41
43
  },
42
44
  };
43
45
  export function loadConfig() {
package/dist/index.js CHANGED
@@ -51,15 +51,15 @@ const chatIdArg = getArg('--chat', null);
51
51
  // переотправляется (он уже есть в начале чата). Флаг --resend-prompt
52
52
  // заставляет дослать его заново — например, если промпт обновился.
53
53
  const resendPrompt = hasFlag('--resend-prompt');
54
- // --new-chat: намеренно начать с чистого чата, игнорируя сохранённую сессию.
55
- // --resume-last: оставлен для совместимости (восстановление и так по умолчанию).
56
- // По умолчанию последняя сессия для рабочей директории восстанавливается.
54
+ // По умолчанию при старте начинается НОВЫЙ чат (контекст не тянется).
55
+ // --resume-last: вернуться в последнюю сессию для рабочей директории.
56
+ // --new-chat: оставлен для совместимости — это и так поведение по умолчанию.
57
57
  const newChatFlag = hasFlag('--new-chat');
58
58
  const resumeLastFlag = hasFlag('--resume-last');
59
59
  // ---------- session persistence ----------
60
60
  // Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
61
- // перезапуск процесса. При старте из той же рабочей директории последняя
62
- // сессия восстанавливается автоматически (если не передан --new-chat).
61
+ // перезапуск и были доступны для явного восстановления (--resume-last,
62
+ // --chat <id>, /resume <n>). Автоматически при старте они НЕ поднимаются.
63
63
  // Раньше здесь был один файл last-chat.json, который терялся при смене
64
64
  // проекта и не давал списка сессий для восстановления.
65
65
  function saveLastChat(id, workdir = '', title = '') {
@@ -168,8 +168,8 @@ ${theme.bold('Опции CLI:')}
168
168
  --dir <path> рабочая директория агента
169
169
  --task <text> задача одной строкой
170
170
  --chat <id> продолжить существующий чат по id
171
- --resume-last вернуться в последний сохранённый чат (по умолчанию)
172
- --new-chat начать новый чат, не восстанавливать сессию
171
+ --resume-last вернуться в последний сохранённый чат
172
+ --new-chat начать новый чат (поведение по умолчанию)
173
173
  --resend-prompt дослать system-prompt в существующий чат
174
174
  --max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
175
175
  --headless браузер без UI
@@ -724,10 +724,10 @@ async function main() {
724
724
  const tools = mod.createTools(currentWorkdir, { undo });
725
725
  let freshChat = true;
726
726
  let sendSystemPrompt = true;
727
- // По умолчанию восстанавливаем последнюю сессию для этой директории.
728
- // Начать с чистого листа — --new-chat.
727
+ // По умолчанию начинаем новый чат. Продолжить прошлую сессию —
728
+ // явно: --chat <id> или --resume-last.
729
729
  let resumeId = chatIdArg;
730
- if (!resumeId && !newChatFlag) {
730
+ if (!resumeId && resumeLastFlag && !newChatFlag) {
731
731
  const last = loadLastSession(currentWorkdir);
732
732
  if (last && last.id) {
733
733
  resumeId = last.id;
@@ -786,14 +786,13 @@ async function main() {
786
786
  console.log(theme.system('\nВыход.'));
787
787
  process.exit(0);
788
788
  });
789
- // Восстановление сессии при старте. По умолчанию возвращаемся в последний
790
- // чат для этой рабочей директории (сессии лежат в ~/.zames/.sessions),
791
- // чтобы контекст не терялся после перезапуска. Явно начать с чистого листа
792
- // можно флагом --new-chat.
789
+ // По умолчанию при старте начинаем НОВЫЙ чат: контекст прошлой сессии
790
+ // не тянется автоматически. Продолжить прошлую сессию можно явно:
793
791
  // --chat <id> открыть конкретный чат
794
- // --new-chat не восстанавливать, начать новый чат
792
+ // --resume-last вернуться в последний чат для этой рабочей директории
793
+ // --new-chat оставлен для совместимости (это и так поведение по умолчанию)
795
794
  let resumeId = chatIdArg;
796
- if (!resumeId && !newChatFlag) {
795
+ if (!resumeId && resumeLastFlag && !newChatFlag) {
797
796
  const last = loadLastSession(currentWorkdir);
798
797
  if (last && last.id) {
799
798
  resumeId = last.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {