zames_pro 2.29.1 → 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];
@@ -1063,6 +1081,35 @@ export class DeepSeekBrowser {
1063
1081
  async _isGenerating() {
1064
1082
  return await this._stopButtonVisible();
1065
1083
  }
1084
+ // Is DeepSeek's "Continue" button currently visible? Used to BLOCK the
1085
+ // "answer settled" early-return in the finish loop: while Continue is on
1086
+ // screen the generation is PAUSED, not finished, so returning the partial
1087
+ // text there is exactly the "agent stopped with a Continue button" bug.
1088
+ async _continueButtonVisible() {
1089
+ try {
1090
+ return await this.page.evaluate(() => {
1091
+ const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
1092
+ .replace(/\s+/g, ' ')
1093
+ .trim();
1094
+ const isExact = (t) => /^(continue|продолжить|продолжение)\s*[.!…]?$/i.test(t);
1095
+ const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
1096
+ for (const e of cands) {
1097
+ if (!isExact(labelOf(e)))
1098
+ continue;
1099
+ const st = getComputedStyle(e);
1100
+ if (st.display === 'none' || st.visibility === 'hidden')
1101
+ continue;
1102
+ if (!e.offsetParent && st.position !== 'fixed')
1103
+ continue;
1104
+ return true;
1105
+ }
1106
+ return false;
1107
+ });
1108
+ }
1109
+ catch {
1110
+ return false;
1111
+ }
1112
+ }
1066
1113
  // Click the DeepSeek "Continue" button when it is visible (reasoning/answer
1067
1114
  // paused by the server). Returns true when a click happened.
1068
1115
  //
@@ -1091,6 +1138,27 @@ export class DeepSeekBrowser {
1091
1138
  continue;
1092
1139
  if (!e.offsetParent && st.position !== 'fixed')
1093
1140
  continue;
1141
+ // DeepSeek's button is a React component listening for POINTER/mouse
1142
+ // events, not just a bare click(). A plain e.click() sometimes does
1143
+ // nothing; dispatching the full sequence is what a real press does.
1144
+ const rect = e.getBoundingClientRect();
1145
+ const cx = rect.left + rect.width / 2;
1146
+ const cy = rect.top + rect.height / 2;
1147
+ const opts = {
1148
+ bubbles: true,
1149
+ cancelable: true,
1150
+ composed: true,
1151
+ clientX: cx,
1152
+ clientY: cy,
1153
+ button: 0,
1154
+ };
1155
+ try {
1156
+ e.dispatchEvent(new PointerEvent('pointerdown', opts));
1157
+ e.dispatchEvent(new MouseEvent('mousedown', opts));
1158
+ e.dispatchEvent(new PointerEvent('pointerup', opts));
1159
+ e.dispatchEvent(new MouseEvent('mouseup', opts));
1160
+ }
1161
+ catch { }
1094
1162
  e.click();
1095
1163
  return true;
1096
1164
  }
@@ -1583,6 +1651,7 @@ export class DeepSeekBrowser {
1583
1651
  }
1584
1652
  this._netCapture = '';
1585
1653
  this._netCaptureAt = 0;
1654
+ this._netNoAnswer = false;
1586
1655
  // Align the DeepSeek chat toggles (deep thinking / web search) with the
1587
1656
  // configured state BEFORE typing. Doing it here (after the send-pause) it
1588
1657
  // does not flip toggles for a generation that is not going to happen.
@@ -1647,6 +1716,19 @@ export class DeepSeekBrowser {
1647
1716
  isGenerationIncompleteText(this._netCapture)) {
1648
1717
  throw new GenerationIncompleteError(this._netCapture.slice(-300));
1649
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
+ }
1650
1732
  // Reasoning-mode pause during the THINK phase: click Continue so the
1651
1733
  // generation resumes (the button appears before any RESPONSE text, so
1652
1734
  // the start-wait would otherwise spin until the deadline).
@@ -1688,7 +1770,10 @@ export class DeepSeekBrowser {
1688
1770
  // capture proves a new answer exists. If it stays equal, keep waiting.
1689
1771
  const notGenerating = !(await this._isGenerating());
1690
1772
  const differs = !!cur && normText(cur) !== normText(beforeText);
1691
- if (differs && cur === lastStartCur && notGenerating) {
1773
+ // Do NOT accept a "settled" answer while Continue is on screen — that
1774
+ // is a paused generation, not a finished one.
1775
+ const paused = this.autoContinue && (await this._continueButtonVisible());
1776
+ if (differs && cur === lastStartCur && notGenerating && !paused) {
1692
1777
  settledTicks++;
1693
1778
  if (settledTicks >= 2) {
1694
1779
  this._askDebug('SETTLED-differs return len=' + cur.length);
@@ -1795,6 +1880,20 @@ export class DeepSeekBrowser {
1795
1880
  isGenerationIncompleteText(this._netCapture)) {
1796
1881
  throw new GenerationIncompleteError(this._netCapture.slice(-300));
1797
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
+ }
1798
1897
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
1799
1898
  // Continue button; the model does not resume by itself. Click it so the
1800
1899
  // answer keeps flowing instead of waiting for the operator.
@@ -1829,7 +1928,17 @@ export class DeepSeekBrowser {
1829
1928
  // so the FIN-loop always cost ~1.6s per answer. Defaults are now
1830
1929
  // 2 checks x 400ms (~0.8s saved per turn) and the values are honored.
1831
1930
  if (stable >= Math.max(1, this.stabilityChecks - 1)) {
1832
- if (isNew || !(await this._isGenerating())) {
1931
+ // A PAUSED generation also looks "stable" (the text stops changing
1932
+ // while Continue is on screen), so the old check returned the
1933
+ // partial answer here — the "agent stopped with a Continue button"
1934
+ // symptom. Click Continue and keep waiting instead.
1935
+ if (this.autoContinue && (await this._continueButtonVisible())) {
1936
+ if (await this._clickContinueIfVisible()) {
1937
+ this._askDebug('CLICKED Continue (before early return)');
1938
+ }
1939
+ stable = 0;
1940
+ }
1941
+ else if (isNew || !(await this._isGenerating())) {
1833
1942
  this._askDebug('RETURN stable curLen=' + cur.length);
1834
1943
  return cur;
1835
1944
  }
@@ -1854,7 +1963,21 @@ export class DeepSeekBrowser {
1854
1963
  netFresh);
1855
1964
  await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
1856
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
+ }
1857
1976
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
1977
+ // If Continue is STILL on screen, the turn is paused, not finished:
1978
+ // click once more and report what we have rather than silently stopping.
1979
+ if (this.autoContinue)
1980
+ await this._clickContinueIfVisible();
1858
1981
  this._askDebug('RETURN last len=' + last.length);
1859
1982
  return last;
1860
1983
  }
package/dist/input.js CHANGED
@@ -233,6 +233,10 @@ export class LineEditor {
233
233
  pendingText;
234
234
  rendered;
235
235
  cursorRowFromTop;
236
+ // Number of rows occupied by the status/spinner block above the input line,
237
+ // as computed by the last _writeBlock(). _renderInputOnly() uses it to keep
238
+ // cursorRowFromTop correct without repainting the status.
239
+ _statusTop;
236
240
  busy;
237
241
  onSubmit;
238
242
  onEscape;
@@ -309,6 +313,7 @@ export class LineEditor {
309
313
  this.pendingText = null;
310
314
  this.rendered = false;
311
315
  this.cursorRowFromTop = 0;
316
+ this._statusTop = 0;
312
317
  this.busy = false;
313
318
  this.onSubmit = null;
314
319
  this.onEscape = null;
@@ -642,6 +647,7 @@ export class LineEditor {
642
647
  // re-printing the whole block on every keystroke is what made the display
643
648
  // flicker on terminals like Tabby. `top` is recovered from the cache so the
644
649
  // cursor math stays correct.
650
+ this._statusTop = top;
645
651
  const statusBlock = statusOut;
646
652
  const statusUnchanged = this.rendered && statusBlock === this._lastStatusBlock;
647
653
  if (statusUnchanged) {
@@ -729,6 +735,52 @@ export class LineEditor {
729
735
  this._eraseBlock();
730
736
  this._writeBlock();
731
737
  }
738
+ // Repaint ONLY the input rows, leaving the status/spinner block untouched.
739
+ // Used for buffer/cursor changes (typing, arrows, backspace, …): the status
740
+ // line has its OWN animation timer, and repainting it on every keystroke
741
+ // made the whole block blink on terminals like Tabby. When nothing is drawn
742
+ // yet (rendered === false) fall back to a full render so the very first
743
+ // paint is correct.
744
+ _renderInputOnly() {
745
+ if (!this.rendered) {
746
+ this._render();
747
+ return;
748
+ }
749
+ const cols = process.stdout.columns || 80;
750
+ const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
751
+ const inputRows = lay.rows.length;
752
+ const sugg = this._suggestions();
753
+ const shown = sugg.slice(0, 8);
754
+ this._suggestCount = shown.length;
755
+ const linesBelow = shown.length
756
+ ? shown.length + (sugg.length > shown.length ? 1 : 0)
757
+ : 0;
758
+ this._eraseInputOnly(inputRows + linesBelow);
759
+ let out = lay.rows.map((r) => r.prefix + r.text).join(NL);
760
+ if (shown.length) {
761
+ const maxName = Math.max(...shown.map((c) => c.name.length));
762
+ const lines = shown.map((c) => {
763
+ const name = theme.prompt(c.name.padEnd(maxName));
764
+ const desc = theme.dim(' ' + c.description);
765
+ return ' ' + name + desc;
766
+ });
767
+ out += NL + lines.join(NL);
768
+ const hidden = sugg.length - shown.length;
769
+ if (hidden > 0)
770
+ out +=
771
+ NL +
772
+ theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
773
+ }
774
+ process.stdout.write(out);
775
+ const up = lay.rows.length - 1 - lay.cursorRow + linesBelow;
776
+ if (up > 0)
777
+ process.stdout.write(ESC + '[' + up + 'A');
778
+ process.stdout.write(CR);
779
+ if (lay.cursorCol > 0)
780
+ process.stdout.write(ESC + '[' + lay.cursorCol + 'C');
781
+ this.rendered = true;
782
+ this.cursorRowFromTop = this._statusTop + lay.cursorRow;
783
+ }
732
784
  // Print a block ABOVE the input line and put the input line back.
733
785
  printAbove(text) {
734
786
  this._stopDots();
@@ -1185,7 +1237,7 @@ export class LineEditor {
1185
1237
  }
1186
1238
  this._pasteBuf = '';
1187
1239
  }
1188
- this._render();
1240
+ this._renderInputOnly();
1189
1241
  continue;
1190
1242
  }
1191
1243
  const pasteAt = s.indexOf(PASTE_START);
@@ -1196,7 +1248,7 @@ export class LineEditor {
1196
1248
  this._pasteBuf = '';
1197
1249
  if (before) {
1198
1250
  this._insert(before);
1199
- this._render();
1251
+ this._renderInputOnly();
1200
1252
  }
1201
1253
  continue;
1202
1254
  }
@@ -1205,31 +1257,31 @@ export class LineEditor {
1205
1257
  if (s.startsWith(ESC + '[13;2u')) {
1206
1258
  this._insertNewline();
1207
1259
  s = s.slice(7);
1208
- this._render();
1260
+ this._renderInputOnly();
1209
1261
  continue;
1210
1262
  }
1211
1263
  if (s.startsWith(ESC + '[13;5u')) {
1212
1264
  this._insertNewline();
1213
1265
  s = s.slice(7);
1214
- this._render();
1266
+ this._renderInputOnly();
1215
1267
  continue;
1216
1268
  }
1217
1269
  if (s.startsWith(ESC + '[27;2;13~')) {
1218
1270
  this._insertNewline();
1219
1271
  s = s.slice(10);
1220
- this._render();
1272
+ this._renderInputOnly();
1221
1273
  continue;
1222
1274
  }
1223
1275
  if (s.startsWith(ESC + '[27;5;13~')) {
1224
1276
  this._insertNewline();
1225
1277
  s = s.slice(10);
1226
- this._render();
1278
+ this._renderInputOnly();
1227
1279
  continue;
1228
1280
  }
1229
1281
  if (s.startsWith(ESC + NL)) {
1230
1282
  this._insertNewline();
1231
1283
  s = s.slice(2);
1232
- this._render();
1284
+ this._renderInputOnly();
1233
1285
  continue;
1234
1286
  }
1235
1287
  const ch = s[0];
@@ -1243,7 +1295,7 @@ export class LineEditor {
1243
1295
  const before = this.cursor > 0 ? Array.from(this.buf)[this.cursor - 1] : '';
1244
1296
  if (before === '\\') {
1245
1297
  this._insertNewline();
1246
- this._render();
1298
+ this._renderInputOnly();
1247
1299
  continue;
1248
1300
  }
1249
1301
  this._submit();
@@ -1251,7 +1303,7 @@ export class LineEditor {
1251
1303
  }
1252
1304
  if (code === 10) {
1253
1305
  this._insertNewline();
1254
- this._render();
1306
+ this._renderInputOnly();
1255
1307
  continue;
1256
1308
  }
1257
1309
  if (code === 3) {
@@ -1266,24 +1318,24 @@ export class LineEditor {
1266
1318
  }
1267
1319
  if (code === 1) {
1268
1320
  this._home();
1269
- this._render();
1321
+ this._renderInputOnly();
1270
1322
  continue;
1271
1323
  }
1272
1324
  if (code === 5) {
1273
1325
  this._end();
1274
- this._render();
1326
+ this._renderInputOnly();
1275
1327
  continue;
1276
1328
  }
1277
1329
  if (code === 9) {
1278
1330
  this._completeCommand();
1279
- this._render();
1331
+ this._renderInputOnly();
1280
1332
  continue;
1281
1333
  }
1282
1334
  if (code === 21) {
1283
1335
  this.buf = '';
1284
1336
  this.cursor = 0;
1285
1337
  this.pastes = [];
1286
- this._render();
1338
+ this._renderInputOnly();
1287
1339
  continue;
1288
1340
  }
1289
1341
  if (code === 22) {
@@ -1295,7 +1347,7 @@ export class LineEditor {
1295
1347
  }
1296
1348
  if (code === 23) {
1297
1349
  this._deleteWordLeft();
1298
- this._render();
1350
+ this._renderInputOnly();
1299
1351
  continue;
1300
1352
  }
1301
1353
  if (code === 11) {
@@ -1306,12 +1358,12 @@ export class LineEditor {
1306
1358
  e++;
1307
1359
  arr.splice(this.cursor, e - this.cursor);
1308
1360
  this.buf = arr.join('');
1309
- this._render();
1361
+ this._renderInputOnly();
1310
1362
  continue;
1311
1363
  }
1312
1364
  if (code === 127 || code === 8) {
1313
1365
  this._backspace();
1314
- this._render();
1366
+ this._renderInputOnly();
1315
1367
  continue;
1316
1368
  }
1317
1369
  if (code === 27) {
@@ -1320,25 +1372,25 @@ export class LineEditor {
1320
1372
  if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
1321
1373
  this._wordLeft();
1322
1374
  s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
1323
- this._render();
1375
+ this._renderInputOnly();
1324
1376
  continue;
1325
1377
  }
1326
1378
  if (s.startsWith('[1;5C') || s.startsWith('[5C')) {
1327
1379
  this._wordRight();
1328
1380
  s = s.slice(s.startsWith('[1;5C') ? 5 : 3);
1329
- this._render();
1381
+ this._renderInputOnly();
1330
1382
  continue;
1331
1383
  }
1332
1384
  if (s.startsWith('[D')) {
1333
1385
  this._left();
1334
1386
  s = s.slice(2);
1335
- this._render();
1387
+ this._renderInputOnly();
1336
1388
  continue;
1337
1389
  }
1338
1390
  if (s.startsWith('[C')) {
1339
1391
  this._right();
1340
1392
  s = s.slice(2);
1341
- this._render();
1393
+ this._renderInputOnly();
1342
1394
  continue;
1343
1395
  }
1344
1396
  if (s.startsWith('[A')) {
@@ -1348,7 +1400,7 @@ export class LineEditor {
1348
1400
  else
1349
1401
  this._up();
1350
1402
  s = s.slice(2);
1351
- this._render();
1403
+ this._renderInputOnly();
1352
1404
  continue;
1353
1405
  }
1354
1406
  if (s.startsWith('[B')) {
@@ -1357,38 +1409,38 @@ export class LineEditor {
1357
1409
  else
1358
1410
  this._down();
1359
1411
  s = s.slice(2);
1360
- this._render();
1412
+ this._renderInputOnly();
1361
1413
  continue;
1362
1414
  }
1363
1415
  if (s.startsWith('[H') || s.startsWith('[1~')) {
1364
1416
  this._home();
1365
1417
  s = s.slice(s.startsWith('[1~') ? 3 : 2);
1366
- this._render();
1418
+ this._renderInputOnly();
1367
1419
  continue;
1368
1420
  }
1369
1421
  if (s.startsWith('[F') || s.startsWith('[4~')) {
1370
1422
  this._end();
1371
1423
  s = s.slice(s.startsWith('[4~') ? 3 : 2);
1372
- this._render();
1424
+ this._renderInputOnly();
1373
1425
  continue;
1374
1426
  }
1375
1427
  if (s.startsWith('[3~')) {
1376
1428
  this._delete();
1377
1429
  s = s.slice(3);
1378
- this._render();
1430
+ this._renderInputOnly();
1379
1431
  continue;
1380
1432
  }
1381
1433
  // Alt+B / Alt+F — word movement.
1382
1434
  if (s.startsWith('b') || s.startsWith('B')) {
1383
1435
  this._wordLeft();
1384
1436
  s = s.slice(1);
1385
- this._render();
1437
+ this._renderInputOnly();
1386
1438
  continue;
1387
1439
  }
1388
1440
  if (s.startsWith('f') || s.startsWith('F')) {
1389
1441
  this._wordRight();
1390
1442
  s = s.slice(1);
1391
- this._render();
1443
+ this._renderInputOnly();
1392
1444
  continue;
1393
1445
  }
1394
1446
  let j = 0;
@@ -1400,7 +1452,7 @@ export class LineEditor {
1400
1452
  if (code < 32)
1401
1453
  continue;
1402
1454
  this._insert(ch);
1403
- this._render();
1455
+ this._renderInputOnly();
1404
1456
  }
1405
1457
  }
1406
1458
  }
@@ -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.1",
3
+ "version": "2.29.3",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {