zames_pro 2.29.2 → 2.29.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.
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { chromium, } from 'playwright';
2
- import { extractAnswer, extractTokenUsage, dumpNetBody } from './net-capture.js';
2
+ import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import fs from 'fs/promises';
@@ -317,6 +317,10 @@ export class DeepSeekBrowser {
317
317
  page;
318
318
  _netCapture;
319
319
  _netCaptureAt;
320
+ // True when the LAST response body was a FINISHED turn with NO answer text
321
+ // (only reasoning). The DOM keeps showing reasoning/old text, so the loop
322
+ // must not wait it out — it clicks Continue / resends. Reset before a send.
323
+ _netNoAnswer;
320
324
  _netChatId;
321
325
  // The latest CONTEXT size (in tokens) DeepSeek reported in the current
322
326
  // chat. It is the `accumulated_token_usage` counter taken from the SSE
@@ -336,6 +340,13 @@ export class DeepSeekBrowser {
336
340
  _lastHistoryError;
337
341
  _netSniff;
338
342
  _netSniffLimit;
343
+ // The RAW SSE body of the LAST answer response. Unlike `_netCapture`, this
344
+ // is kept even when the body carries no RESPONSE text (a truncated turn:
345
+ // generation_err / INCOMPLETE, or FINISHED-with-only-reasoning). The
346
+ // incomplete/no-answer detectors MUST run on the raw body — guarding them on
347
+ // `_netCapture` (which is '' in exactly those cases) made them dead code and
348
+ // the agent hung until the timeout.
349
+ _netBody;
339
350
  _netHookInstalled;
340
351
  // Fired right when a message is actually typed/sent (AFTER the send-pause
341
352
  // and attachments). Used to start the "agent is working" spinner only when
@@ -380,6 +391,7 @@ export class DeepSeekBrowser {
380
391
  this._stopped = false;
381
392
  this._netCapture = '';
382
393
  this._netCaptureAt = 0;
394
+ this._netNoAnswer = false;
383
395
  this._netChatId = null;
384
396
  this._lastTokenUsage = null;
385
397
  this._toggles = { deepThinking, webSearch };
@@ -388,6 +400,7 @@ export class DeepSeekBrowser {
388
400
  this._lastHistoryError = '';
389
401
  this._netSniff = [];
390
402
  this._netSniffLimit = 5;
403
+ this._netBody = '';
391
404
  this._netHookInstalled = false;
392
405
  this.onSendStart = null;
393
406
  this.onSendPause = null;
@@ -543,11 +556,38 @@ export class DeepSeekBrowser {
543
556
  const extracted = extractAnswer(body);
544
557
  if (extracted) {
545
558
  this._netCapture = extracted;
559
+ this._netNoAnswer = false;
546
560
  const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
547
561
  if (cid)
548
562
  this._netChatId = cid[0];
549
563
  this._netCaptureAt = Date.now();
550
564
  }
565
+ // Store the RAW body for the CURRENT answer (completion/continue) so the
566
+ // truncated/no-answer detectors can inspect it even when there is no
567
+ // RESPONSE text (extractAnswer() returns '' for those bodies).
568
+ if (/chat\/(completion|continue)/i.test(url)) {
569
+ this._netBody = body;
570
+ const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
571
+ if (cid)
572
+ this._netChatId = cid[0];
573
+ if (!extracted) {
574
+ if (isFinishedWithoutAnswer(body)) {
575
+ // A FINISHED turn with NO RESPONSE fragment (only reasoning): the
576
+ // UI shows Stopped + Continue. Record it with a fresh timestamp
577
+ // so the ask() loops do not wait out the timeout — they click
578
+ // Continue / resend.
579
+ this._netNoAnswer = true;
580
+ this._netCapture = '';
581
+ }
582
+ // A truncated turn (generation_err / INCOMPLETE) also has no
583
+ // RESPONSE; make the body visible to the incomplete check by
584
+ // refreshing the timestamp.
585
+ if (isGenerationIncompleteText(body)) {
586
+ this._netCapture = '';
587
+ }
588
+ this._netCaptureAt = Date.now();
589
+ }
590
+ }
551
591
  }
552
592
  catch { }
553
593
  }
@@ -1633,6 +1673,8 @@ export class DeepSeekBrowser {
1633
1673
  }
1634
1674
  this._netCapture = '';
1635
1675
  this._netCaptureAt = 0;
1676
+ this._netNoAnswer = false;
1677
+ this._netBody = '';
1636
1678
  // Align the DeepSeek chat toggles (deep thinking / web search) with the
1637
1679
  // configured state BEFORE typing. Doing it here (after the send-pause) it
1638
1680
  // does not flip toggles for a generation that is not going to happen.
@@ -1692,10 +1734,26 @@ export class DeepSeekBrowser {
1692
1734
  // The turn was truncated by the server: the SSE body carries
1693
1735
  // finish_reason=generation_err / quasi_status=INCOMPLETE and the UI
1694
1736
  // shows "Continue". Retry instead of waiting out the whole timeout.
1695
- if (this._netCapture &&
1696
- this._netCaptureAt >= this._lastSentAt &&
1697
- isGenerationIncompleteText(this._netCapture)) {
1698
- throw new GenerationIncompleteError(this._netCapture.slice(-300));
1737
+ // NOTE: the check runs on the RAW `_netBody`, not on `_netCapture`: a
1738
+ // truncated body has NO RESPONSE fragment, so extractAnswer() returned
1739
+ // '' and `_netCapture` was empty — guarding on it made this branch dead
1740
+ // code and the agent hung on "Stopped" until the timeout.
1741
+ if (this._netCaptureAt >= this._lastSentAt &&
1742
+ isGenerationIncompleteText(this._netBody)) {
1743
+ throw new GenerationIncompleteError(this._netBody.slice(-300));
1744
+ }
1745
+ // A FINISHED turn with NO answer (only reasoning): the UI shows a
1746
+ // Continue button. Click it (or, if autoContinue is off, keep waiting)
1747
+ // instead of spinning to the deadline on an empty answer.
1748
+ if (this._netNoAnswer && this._netCaptureAt >= this._lastSentAt) {
1749
+ this._askDebug('START-loop net-no-answer');
1750
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1751
+ this._askDebug('CLICKED Continue (net-no-answer, start-loop)');
1752
+ this._netNoAnswer = false;
1753
+ this._netCaptureAt = 0;
1754
+ await this.page.waitForTimeout(500);
1755
+ continue;
1756
+ }
1699
1757
  }
1700
1758
  // Reasoning-mode pause during the THINK phase: click Continue so the
1701
1759
  // generation resumes (the button appears before any RESPONSE text, so
@@ -1773,6 +1831,14 @@ export class DeepSeekBrowser {
1773
1831
  // previous tool call.
1774
1832
  const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
1775
1833
  const fresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
1834
+ // The turn was truncated / produced no answer (generation_err,
1835
+ // INCOMPLETE, or FINISHED-with-only-reasoning): the message WAS sent,
1836
+ // the server just cut the turn. Resend (ask() handles it) instead of
1837
+ // treating this as "send failed" and hammering Enter.
1838
+ if (this._netCaptureAt >= this._lastSentAt &&
1839
+ (isGenerationIncompleteText(this._netBody) || this._netNoAnswer)) {
1840
+ throw new GenerationIncompleteError(this._netBody.slice(-300));
1841
+ }
1776
1842
  if (cur &&
1777
1843
  cur.trim() &&
1778
1844
  normText(cur) !== normText(beforeText) &&
@@ -1843,10 +1909,25 @@ export class DeepSeekBrowser {
1843
1909
  // keeps the partial answer, so without this check the loop waits out the
1844
1910
  // whole timeout and throws ds.send_no_new_answer — the operator sees the
1845
1911
  // agent "stop" with a Continue button in the chat. Retry the send.
1846
- if (this._netCapture &&
1912
+ // Runs on the RAW `_netBody` (a truncated body has no RESPONSE, so
1913
+ // `_netCapture` is empty and the old guard was dead code).
1914
+ if (this._netCaptureAt >= this._lastSentAt &&
1915
+ isGenerationIncompleteText(this._netBody)) {
1916
+ throw new GenerationIncompleteError(this._netBody.slice(-300));
1917
+ }
1918
+ // A FINISHED turn with NO answer (only reasoning): the UI shows a
1919
+ // Continue button. Click it and keep waiting — otherwise the loop spins
1920
+ // to the deadline on an empty answer and the operator sees "Stopped".
1921
+ if (this._netNoAnswer &&
1847
1922
  this._netCaptureAt >= this._lastSentAt &&
1848
- isGenerationIncompleteText(this._netCapture)) {
1849
- throw new GenerationIncompleteError(this._netCapture.slice(-300));
1923
+ this.autoContinue) {
1924
+ if (await this._clickContinueIfVisible()) {
1925
+ this._askDebug('CLICKED Continue (net-no-answer, finish-loop)');
1926
+ this._netNoAnswer = false;
1927
+ this._netCaptureAt = 0;
1928
+ await this.page.waitForTimeout(500);
1929
+ continue;
1930
+ }
1850
1931
  }
1851
1932
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
1852
1933
  // Continue button; the model does not resume by itself. Click it so the
@@ -1917,6 +1998,16 @@ export class DeepSeekBrowser {
1917
1998
  netFresh);
1918
1999
  await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
1919
2000
  }
2001
+ // A FINISHED turn with NO answer text (only reasoning) must NOT be
2002
+ // returned as a final answer, and must not hang until the timeout either.
2003
+ // Treat it like a truncated turn: ask() resends the prompt (the same thing
2004
+ // the Continue button does). Only if a real capture arrived in the
2005
+ // meantime do we fall through to return it.
2006
+ if (this._netNoAnswer &&
2007
+ this._netCaptureAt >= this._lastSentAt &&
2008
+ !this._netCapture) {
2009
+ throw new GenerationIncompleteError('finished without answer');
2010
+ }
1920
2011
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
1921
2012
  // If Continue is STILL on screen, the turn is paused, not finished:
1922
2013
  // click once more and report what we have rather than silently stopping.
@@ -159,6 +159,32 @@ export function extractAnswer(body) {
159
159
  return sse;
160
160
  return extractFromJson(body);
161
161
  }
162
+ // True when the SSE body shows a turn that ENDED (`quasi_status: FINISHED`)
163
+ // but produced NO answer text — the reasoning (THINK fragment) was generated
164
+ // and then the model stopped WITHOUT writing a RESPONSE fragment.
165
+ //
166
+ // This is the "Stopped + Continue button" case: DeepSeek finishes the turn
167
+ // with only reasoning, the UI shows a Continue button, and extractAnswer()
168
+ // returns '' (it only collects RESPONSE fragments). The old code then saw an
169
+ // empty capture, ignored it, and the finish loop waited out the whole timeout
170
+ // and threw ds.send_no_new_answer — to the operator the agent "hung on Stopped".
171
+ // Unlike generation_err, `quasi_status` here is FINISHED, so
172
+ // isGenerationIncompleteText() does NOT catch it.
173
+ //
174
+ export function isFinishedWithoutAnswer(body) {
175
+ const b = String(body || '');
176
+ if (!b)
177
+ return false;
178
+ const finished = /"quasi_status","v":"FINISHED"/i.test(b);
179
+ if (!finished)
180
+ return false;
181
+ // A RESPONSE fragment with actual content means the model DID answer.
182
+ if (/"type":"RESPONSE","content":"[^"]/.test(b))
183
+ return false;
184
+ // The turn carried reasoning (THINK) or was truncated mid-way; treat the
185
+ // absence of response content as "no answer produced".
186
+ return true;
187
+ }
162
188
  // The CONTEXT size (in tokens) DeepSeek reports for the current answer.
163
189
  //
164
190
  // chat.deepseek.com does not expose prompt_tokens/completion_tokens the way
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.29.2",
3
+ "version": "2.29.4",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {