zames_pro 2.10.7 → 2.10.8

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.
@@ -117,6 +117,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
117
117
  // instead get an EMPTY answer or the EXACT same answer as the previous turn
118
118
  // (a sign the new message was not sent), we nudge instead of stopping.
119
119
  let lastRaw = '';
120
+ // True when the PREVIOUS turn actually executed a tool. This is what
121
+ // makes a repeated answer "stale": the tool already ran, so running it
122
+ // again would duplicate side effects. If the previous answer looked like a
123
+ // call but was not recognized (e.g. a DSML/XML form the strict parser
124
+ // missed), the tool did NOT run and the echo is the only copy of the
125
+ // call — it must be executed, not discarded as stale.
126
+ // const ranToolLastTurn follows the previous turn's actual effect.
127
+ let lastTurnRanTool = false;
120
128
  let justRanTool = false;
121
129
  let watchdogRetries = 0;
122
130
  const MAX_WATCHDOG_RETRIES = 3;
@@ -131,6 +139,11 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
131
139
  // Subsequent ones (tool-result and resend requests) are agent sends:
132
140
  // throttled so we don't hit the rate limit.
133
141
  const isFirst = i === 0;
142
+ // Did the PREVIOUS turn actually execute a tool? Used by the stale
143
+ // check below. We take a snapshot and immediately clear it: it will be
144
+ // set again at the end of this iteration only if a tool really runs.
145
+ const ranToolPrevTurn = lastTurnRanTool;
146
+ lastTurnRanTool = false;
134
147
  // Safety net: browser.ask() has its own timeout, but a stuck send used to
135
148
  // block the whole loop and look like a silent stop. We race it against a
136
149
  // hard deadline and treat a timeout as a nudge (re-ask), never as a
@@ -194,18 +207,31 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
194
207
  // previous answer with different markdown emphasis (`**done**` vs `done`),
195
208
  // which defeated an exact match and made the loop run the SAME tool again —
196
209
  // the "stopped after a tool call" signature with a duplicate call.
197
- const wdStale = justRanTool &&
210
+ //
211
+ // AN ECHO IS NOT ALWAYS STALE: the previous answer may have been a
212
+ // DSML/XML call that the strict parser missed and the loop never actually
213
+ // ran. In that case the echo is the ONLY copy of the call we can get,
214
+ // and discarding it as "stale" burns the watchdog budget and stalls the
215
+ // agent. So we first check whether the echo itself parses as a tool
216
+ // call (including the XML/DSML form); if it does, we do NOT treat it
217
+ // as stale — the call gets run below like any other.
218
+ // Stale means: the PREVIOUS turn actually ran a tool (lastTurnRanTool)
219
+ // and this answer is a repeat of the previous one. If the previous
220
+ // answer never ran a tool (unrecognized DSML call), the repeat is NOT
221
+ // stale — it is the only copy of the call and must be executed.
222
+ const staleCandidate = ranToolPrevTurn &&
198
223
  lastRaw.trim() !== '' &&
199
224
  (rawResponse.trim() === lastRaw.trim() ||
200
225
  normForStale(rawResponse) === normForStale(lastRaw));
226
+ const wdStale = staleCandidate;
201
227
  const wdNoCall = justRanTool &&
202
228
  !wdEmpty &&
203
229
  !wdStale &&
204
230
  parseToolCall(rawResponse) === null &&
205
231
  !responseLooksLikeToolCall(rawResponse);
206
- // A stale answer is discarded even when it parses to a valid call: it is a
207
- // duplicate of the previous turn, and re-running the tool would repeat
208
- // side effects and stall the loop.
232
+ // A stale answer is discarded only when it does NOT parse as a tool
233
+ // call (see wdStale above); an echo of a call that never ran is a real
234
+ // call and must be executed, not dropped.
209
235
  if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
210
236
  watchdogRetries++;
211
237
  transcript?.log('watchdog_nudge', {
@@ -464,6 +490,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
464
490
  // A tool just ran — the next answer is expected to be a fresh tool call.
465
491
  // Reset the watchdog so the next empty/repeated answer is nudged.
466
492
  justRanTool = true;
493
+ // The tool really executed on this turn. The NEXT iteration will treat
494
+ // a repeat of this answer as stale only because of this flag (see NEXT,
495
+ // not the current, justRanTool check).
496
+ lastTurnRanTool = true;
467
497
  watchdogRetries = 0;
468
498
  // A fresh tool call just ran: reset the per-tool-result nudge budget so
469
499
  // a long chain of tools is not cut off by an earlier bad turn.
package/dist/browser.js CHANGED
@@ -460,7 +460,11 @@ export class DeepSeekBrowser {
460
460
  throw e;
461
461
  }
462
462
  console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
463
- await this.page.waitForTimeout(this.rateLimitWaitMs);
463
+ // Interruptible: Esc/Ctrl+C must cancel this long wait too,
464
+ // otherwise "stop" stays frozen for up to 5 minutes.
465
+ const aborted = await this._sleepInterruptible(this.rateLimitWaitMs);
466
+ if (aborted)
467
+ return '(прервано пользователем)';
464
468
  continue;
465
469
  }
466
470
  console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
@@ -605,6 +609,12 @@ export class DeepSeekBrowser {
605
609
  // Pause between sends. Applied ONLY to agent messages
606
610
  // (tool-result, system-prompt) so we don't hit the rate limit.
607
611
  // User input is sent without delay.
612
+ //
613
+ // The wait is INTERRUPTIBLE: Esc/Ctrl+C sets _abort, and we check it every
614
+ // 100ms instead of one long page.waitForTimeout(gap). Before, Esc pressed
615
+ // during this pause did nothing to the pause itself — the send was still
616
+ // delayed by the remaining seconds, and the stop request only took effect
617
+ // after the pause. This is the main "Esc does not cancel the pause" bug.
608
618
  async _waitForSendSlot(agent) {
609
619
  if (!agent)
610
620
  return;
@@ -614,7 +624,21 @@ export class DeepSeekBrowser {
614
624
  if (gap <= 0)
615
625
  return;
616
626
  console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой`));
617
- await this.page.waitForTimeout(gap);
627
+ await this._sleepInterruptible(gap);
628
+ }
629
+ // Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
630
+ // Returns true if the sleep was cut short by _abort.
631
+ async _sleepInterruptible(ms) {
632
+ const step = 100;
633
+ let left = ms;
634
+ while (left > 0) {
635
+ if (this._abort)
636
+ return true;
637
+ const chunk = Math.min(step, left);
638
+ await this.page.waitForTimeout(chunk);
639
+ left -= chunk;
640
+ }
641
+ return this._abort;
618
642
  }
619
643
  // DEBUG: append ask() phases to ~/.zames/ask-debug.log so a stall can be
620
644
  // diagnosed from the field (what the DOM/network looked like at each step).
@@ -630,7 +654,12 @@ export class DeepSeekBrowser {
630
654
  }
631
655
  async _askOnce(prompt, { timeout, agent, attachments = [], }) {
632
656
  // We reset the abort flag ONLY at the very start of the send.
657
+ // _stopped (Esc for the WHOLE batch) is NOT reset here: it may have been
658
+ // set while a previous send was waiting in the pause. If it is set, the
659
+ // user asked to stop and we must not start a new generation at all.
633
660
  this._abort = false;
661
+ if (this._stopped)
662
+ return '(прервано пользователем)';
634
663
  const input = await this._findVisible(INPUT_SELECTORS, 10_000);
635
664
  if (!input) {
636
665
  throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
@@ -638,6 +667,9 @@ export class DeepSeekBrowser {
638
667
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
639
668
  this._askDebug('SEND agent=' + agent + ' len=' + prompt.length + ' beforeLen=' + beforeText.length + ' beforeHead=' + JSON.stringify(beforeText.slice(0, 60)));
640
669
  await this._waitForSendSlot(agent);
670
+ // Esc/Ctrl+C pressed during the pause — do not send anything.
671
+ if (this._abort)
672
+ return '(прервано пользователем)';
641
673
  this._netCapture = '';
642
674
  this._netCaptureAt = 0;
643
675
  // Attach files/images FIRST (before the text): the DeepSeek upload widget
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.10.7",
3
+ "version": "2.10.8",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {