zames_pro 2.10.2 → 2.10.4

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.
@@ -2,6 +2,7 @@ import { buildSystemPrompt } from './system-prompt.js';
2
2
  import { getGitContext, formatGitContext } from './gitTools.js';
3
3
  import { parseXmlToolCalls } from './xml-toolcall.js';
4
4
  import { translate } from './i18n.js';
5
+ import { normText } from './browser.js';
5
6
  export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', }) {
6
7
  // UI callbacks must NEVER break the agent loop. A rendering error (a huge
7
8
  // tool result, a broken markdown frame, a closed terminal) used to throw
@@ -189,14 +190,22 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
189
190
  // cut-off "Stale. Let me ..." or a truncated JSON). All three mean the
190
191
  // turn is unfinished: nudge instead of stopping.
191
192
  const wdEmpty = !String(rawResponse || '').trim();
193
+ // STALE is compared on NORMALIZED text: DeepSeek often echoes the
194
+ // previous answer with different markdown emphasis (`**done**` vs `done`),
195
+ // which defeated an exact match and made the loop run the SAME tool again —
196
+ // the "stopped after a tool call" signature with a duplicate call.
192
197
  const wdStale = justRanTool &&
193
198
  lastRaw.trim() !== '' &&
194
- rawResponse.trim() === lastRaw.trim();
199
+ (rawResponse.trim() === lastRaw.trim() ||
200
+ normForStale(rawResponse) === normForStale(lastRaw));
195
201
  const wdNoCall = justRanTool &&
196
202
  !wdEmpty &&
197
203
  !wdStale &&
198
204
  parseToolCall(rawResponse) === null &&
199
205
  !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.
200
209
  if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
201
210
  watchdogRetries++;
202
211
  transcript?.log('watchdog_nudge', {
@@ -522,6 +531,16 @@ function isMeaningfulRespond(msg) {
522
531
  return false;
523
532
  return true;
524
533
  }
534
+ // Normalize an answer for STALE comparison: collapse whitespace AND strip
535
+ // markdown emphasis/code markers. DeepSeek echoes the previous turn with a
536
+ // different emphasis (`**done**` vs `done`), which defeated an exact match and
537
+ // made the loop re-run the same tool ("stopped after a tool call").
538
+ function normForStale(s) {
539
+ return normText(s)
540
+ .replace(/[*_`#>]+/g, '')
541
+ .replace(/[ \t]+/g, ' ')
542
+ .trim();
543
+ }
525
544
  // Text that promises a tool call in the future tense but contains no call
526
545
  // itself. DeepSeek regularly "hangs" like this: it writes
527
546
  // "Now update README to mention …", "Let me run the tests", "I'll check now"
package/dist/browser.js CHANGED
@@ -646,15 +646,23 @@ export class DeepSeekBrowser {
646
646
  .evaluate(() => document.body.innerText.length)
647
647
  .catch(() => 0);
648
648
  let started = false;
649
+ // A stale/echo answer (the model repeats the previous text, or the answer
650
+ // legitimately equals it) does NOT change `cur`. In that case the old loop
651
+ // either threw "did not start" after 15s or hung until the full timeout —
652
+ // the operator saw the agent "stop after a tool call". We now also accept
653
+ // the answer when the generation has clearly SETTLED: no Stop button and
654
+ // the text has been stable for a couple of ticks.
655
+ let settledTicks = 0;
656
+ let lastStartCur = '';
649
657
  while (Date.now() < startDeadline) {
650
658
  if (this._abort)
651
659
  return '(прервано пользователем)';
652
660
  const pageText = await this._readPageText();
653
661
  if (isRateLimitText(pageText)) {
654
662
  throw new RateLimitError(pageText.slice(0, 300));
655
- if (isServerBusyText(pageText)) {
656
- throw new ServerBusyError(pageText.slice(0, 300));
657
- }
663
+ }
664
+ if (isServerBusyText(pageText)) {
665
+ throw new ServerBusyError(pageText.slice(0, 300));
658
666
  }
659
667
  const cur = await this._readLastAnswerTextClean().catch(() => '');
660
668
  const bodyLen = await this.page
@@ -678,9 +686,30 @@ export class DeepSeekBrowser {
678
686
  started = true;
679
687
  break;
680
688
  }
689
+ // Fallback for an echo/stale answer: the send happened (lastSentAt was
690
+ // just updated), the Stop button is gone and the text stopped changing.
691
+ // Two stable ticks in a row mean the turn is over even if it equals the
692
+ // previous text — return it instead of hanging/throwing.
693
+ const notGenerating = !(await this._isGenerating());
694
+ if (cur && cur === lastStartCur && notGenerating && cur.trim()) {
695
+ settledTicks++;
696
+ if (settledTicks >= 2) {
697
+ return cur;
698
+ }
699
+ }
700
+ else {
701
+ settledTicks = 0;
702
+ }
703
+ lastStartCur = cur;
681
704
  await this.page.waitForTimeout(300);
682
705
  }
683
706
  if (!started) {
707
+ // Last chance: the answer may have arrived and settled exactly at the
708
+ // deadline. Return the current text instead of a hard error.
709
+ const cur = await this._readLastAnswerTextClean().catch(() => '');
710
+ if (cur && cur.trim() && !(await this._isGenerating())) {
711
+ return cur;
712
+ }
684
713
  // We NO LONGER check the limit over the whole page text — that caused
685
714
  // false positives and 5-minute waits. We just report that
686
715
  // generation did not start.
@@ -718,21 +747,32 @@ export class DeepSeekBrowser {
718
747
  // belongs to the CURRENT send even if the DOM still shows the old text.
719
748
  const isNew = !!cur &&
720
749
  (netFresh || normText(cur) !== normText(beforeText));
721
- if (isNew && cur === last) {
750
+ // An echo/stale answer equals beforeText, so isNew stays false and the
751
+ // old loop waited until the full timeout — the "agent stopped after a
752
+ // tool call" hang. If generation has clearly ENDED (no Stop button) and
753
+ // the text is stable, accept it (even when it repeats the previous one).
754
+ const sameAsBefore = !!cur && !isNew && normText(cur) === normText(beforeText);
755
+ if ((isNew || sameAsBefore) && cur === last) {
722
756
  stable++;
723
- if (stable >= 2)
724
- return cur;
757
+ if (stable >= 2) {
758
+ if (isNew || !(await this._isGenerating()))
759
+ return cur;
760
+ }
725
761
  }
726
762
  else {
727
763
  stable = 0;
728
764
  }
729
- if (isNew)
765
+ if (isNew || sameAsBefore)
730
766
  last = cur;
731
767
  await this.page.waitForTimeout(800);
732
768
  }
733
769
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
734
770
  return last;
735
771
  }
772
+ // Fallback: the turn settled on a text identical to the previous answer.
773
+ if (last && !(await this._isGenerating())) {
774
+ return last;
775
+ }
736
776
  if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
737
777
  return this._netCapture;
738
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.10.2",
3
+ "version": "2.10.4",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {