zames_pro 2.29.2 → 2.29.3

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
@@ -380,6 +384,7 @@ export class DeepSeekBrowser {
380
384
  this._stopped = false;
381
385
  this._netCapture = '';
382
386
  this._netCaptureAt = 0;
387
+ this._netNoAnswer = false;
383
388
  this._netChatId = null;
384
389
  this._lastTokenUsage = null;
385
390
  this._toggles = { deepThinking, webSearch };
@@ -543,6 +548,19 @@ export class DeepSeekBrowser {
543
548
  const extracted = extractAnswer(body);
544
549
  if (extracted) {
545
550
  this._netCapture = extracted;
551
+ this._netNoAnswer = false;
552
+ const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
553
+ if (cid)
554
+ this._netChatId = cid[0];
555
+ this._netCaptureAt = Date.now();
556
+ }
557
+ else if (/chat\/(completion|continue)/i.test(url) &&
558
+ isFinishedWithoutAnswer(body)) {
559
+ // A FINISHED turn with NO RESPONSE fragment (only reasoning): the UI
560
+ // shows a Continue button. Record it with a fresh timestamp so the
561
+ // ask() loops do not wait out the timeout — they click Continue.
562
+ this._netNoAnswer = true;
563
+ this._netCapture = '';
546
564
  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
565
  if (cid)
548
566
  this._netChatId = cid[0];
@@ -1633,6 +1651,7 @@ export class DeepSeekBrowser {
1633
1651
  }
1634
1652
  this._netCapture = '';
1635
1653
  this._netCaptureAt = 0;
1654
+ this._netNoAnswer = false;
1636
1655
  // Align the DeepSeek chat toggles (deep thinking / web search) with the
1637
1656
  // configured state BEFORE typing. Doing it here (after the send-pause) it
1638
1657
  // does not flip toggles for a generation that is not going to happen.
@@ -1697,6 +1716,19 @@ export class DeepSeekBrowser {
1697
1716
  isGenerationIncompleteText(this._netCapture)) {
1698
1717
  throw new GenerationIncompleteError(this._netCapture.slice(-300));
1699
1718
  }
1719
+ // A FINISHED turn with NO answer (only reasoning): the UI shows a
1720
+ // Continue button. Click it (or, if autoContinue is off, keep waiting)
1721
+ // instead of spinning to the deadline on an empty answer.
1722
+ if (this._netNoAnswer && this._netCaptureAt >= this._lastSentAt) {
1723
+ this._askDebug('START-loop net-no-answer');
1724
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1725
+ this._askDebug('CLICKED Continue (net-no-answer, start-loop)');
1726
+ this._netNoAnswer = false;
1727
+ this._netCaptureAt = 0;
1728
+ await this.page.waitForTimeout(500);
1729
+ continue;
1730
+ }
1731
+ }
1700
1732
  // Reasoning-mode pause during the THINK phase: click Continue so the
1701
1733
  // generation resumes (the button appears before any RESPONSE text, so
1702
1734
  // the start-wait would otherwise spin until the deadline).
@@ -1848,6 +1880,20 @@ export class DeepSeekBrowser {
1848
1880
  isGenerationIncompleteText(this._netCapture)) {
1849
1881
  throw new GenerationIncompleteError(this._netCapture.slice(-300));
1850
1882
  }
1883
+ // A FINISHED turn with NO answer (only reasoning): the UI shows a
1884
+ // Continue button. Click it and keep waiting — otherwise the loop spins
1885
+ // to the deadline on an empty answer and the operator sees "Stopped".
1886
+ if (this._netNoAnswer &&
1887
+ this._netCaptureAt >= this._lastSentAt &&
1888
+ this.autoContinue) {
1889
+ if (await this._clickContinueIfVisible()) {
1890
+ this._askDebug('CLICKED Continue (net-no-answer, finish-loop)');
1891
+ this._netNoAnswer = false;
1892
+ this._netCaptureAt = 0;
1893
+ await this.page.waitForTimeout(500);
1894
+ continue;
1895
+ }
1896
+ }
1851
1897
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
1852
1898
  // Continue button; the model does not resume by itself. Click it so the
1853
1899
  // answer keeps flowing instead of waiting for the operator.
@@ -1917,6 +1963,16 @@ export class DeepSeekBrowser {
1917
1963
  netFresh);
1918
1964
  await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
1919
1965
  }
1966
+ // A FINISHED turn with NO answer text (only reasoning) must NOT be
1967
+ // returned as a final answer, and must not hang until the timeout either.
1968
+ // Treat it like a truncated turn: ask() resends the prompt (the same thing
1969
+ // the Continue button does). Only if a real capture arrived in the
1970
+ // meantime do we fall through to return it.
1971
+ if (this._netNoAnswer &&
1972
+ this._netCaptureAt >= this._lastSentAt &&
1973
+ !this._netCapture) {
1974
+ throw new GenerationIncompleteError('finished without answer');
1975
+ }
1920
1976
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
1921
1977
  // If Continue is STILL on screen, the turn is paused, not finished:
1922
1978
  // 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.3",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {