zames_pro 2.95.1 → 2.95.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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.95.3] - 2026-11-04
11
+
12
+ ### Fixed
13
+
14
+ - **deepseek:** an appended answer is not a dead turn
15
+ - slow start
16
+
17
+ ## [2.95.2] - 2026-11-04
18
+
19
+ ### Changed
20
+
21
+ - fix-deepseek-head-only
22
+ - test: pin the streamed-answer head snapshot (the resend loop)
23
+ - docs: theme gallery, mobile menu and code copy buttons
24
+
10
25
  ## [2.95.1] - 2026-11-03
11
26
 
12
27
  ### Fixed
package/dist/browser.js CHANGED
@@ -510,6 +510,12 @@ export class DeepSeekBrowser {
510
510
  this._lastSentAt = Date.now();
511
511
  }
512
512
  this._netBody = body;
513
+ // The answer clock drives the "is this body FRESH?" gate in both
514
+ // ask loops. A body that carries a (head) fragment but no decodable
515
+ // answer text left the clock at 0, so every detector below saw a
516
+ // stale body and stayed silent. Give it a fresh timestamp.
517
+ if (!this._netCaptureAt)
518
+ this._netCaptureAt = Date.now();
513
519
  const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
514
520
  if (cid)
515
521
  this._netChatId = cid[0];
@@ -2291,11 +2297,15 @@ export class DeepSeekBrowser {
2291
2297
  // Wait for the start: either Stop appeared, or the answer text changed,
2292
2298
  // or the page grew (a new answer node / longer last answer). In parallel we
2293
2299
  // catch the rate-limit toast (only toasts, not the whole body).
2294
- // MEASURED live: an answer either starts within a few seconds or the send
2295
- // did not go out at all. The old 30s + 20s windows meant FIFTY seconds of
2296
- // a frozen "generating" status on every missed start; 12s + 10s is
2297
- // plenty for DeepSeek to show the Stop button or start streaming.
2298
- const startDeadline = Date.now() + 12_000;
2300
+ //
2301
+ // MEASURED on 90 live turns: the server needs a MEDIAN of ~16s to deliver a
2302
+ // completion body (min 11.7s, max 25.8s), so the old 12s window was shorter
2303
+ // than a NORMAL turn. The loop then declared "the send did not go out" and
2304
+ // pressed Enter AGAIN — a DUPLICATE turn that ended in "Messages too
2305
+ // frequent". The window only has to cover the SEND (the input clearing, the
2306
+ // Stop button appearing), not the answer, so 30s is generous without
2307
+ // delaying a real miss by minutes.
2308
+ const startDeadline = Date.now() + 30_000;
2299
2309
  // A CHEAP growth signal (message-node count + last-answer length)
2300
2310
  // instead of document.body.innerText.length, which walked the whole page.
2301
2311
  const startSignal = await this._chatSignal();
@@ -2308,6 +2318,7 @@ export class DeepSeekBrowser {
2308
2318
  // the text has been stable for a couple of ticks.
2309
2319
  let settledTicks = 0;
2310
2320
  let lastStartCur = '';
2321
+ let stopTicks = 0;
2311
2322
  while (Date.now() < startDeadline) {
2312
2323
  if (this._abort || this._askCancelled)
2313
2324
  return '(прервано пользователем)';
@@ -2435,7 +2446,20 @@ export class DeepSeekBrowser {
2435
2446
  // 'changed' can stay false for a while — without this check ask() used
2436
2447
  // to hang until the full timeout and the agent appeared to "stop".
2437
2448
  const netStarted = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
2438
- if (changed || netStarted || grew) {
2449
+ // The Stop button is the SERVER's own "a turn is running" signal, and it
2450
+ // appears long before the first token reaches the DOM or the completion
2451
+ // body is complete (measured: the body lands a median 16s after the
2452
+ // send, while Stop shows up within a second). Without it a SLOW network
2453
+ // looked like a failed send: the loop hit the start deadline and pressed
2454
+ // Enter again, producing a duplicate turn. A lingering Stop from the
2455
+ // previous generation is ruled out by requiring TWO consecutive ticks
2456
+ // with the button visible, so a single stray paint does not start us.
2457
+ const stopVisible = await this._isGenerating();
2458
+ if (stopVisible)
2459
+ stopTicks++;
2460
+ else
2461
+ stopTicks = 0;
2462
+ if (changed || netStarted || grew || stopTicks >= 2) {
2439
2463
  started = true;
2440
2464
  this._askDebug('STARTED changed=' +
2441
2465
  changed +
@@ -2631,6 +2655,15 @@ export class DeepSeekBrowser {
2631
2655
  const noAnswer = !doneAnswer &&
2632
2656
  this._netNoAnswer &&
2633
2657
  this._netCaptureAt >= this._lastSentAt;
2658
+ // A HEAD-ONLY snapshot is NOT a dead turn: DeepSeek opens a turn
2659
+ // with a RESPONSE fragment that holds only newlines, and the text
2660
+ // arrives in the following bare `data: {"v":...}` chunks.
2661
+ // `_netBody` is the body of the LAST network event, so a poll that
2662
+ // lands between the snapshot and the first chunk sees FINISHED with
2663
+ // no answer, and the old code threw at once (the operator saw
2664
+ // "DeepSeek оборвал ответ") while the chat was still streaming.
2665
+ const headOnly = this._netCaptureAt >= this._lastSentAt &&
2666
+ isFinishedAwaitingAnswer(this._netBody);
2634
2667
  if ((truncated || noAnswer) && this.autoContinue) {
2635
2668
  // MEASURED: a COMPLETE DSML tool call ends with the event `close`
2636
2669
  // and `{click_behavior:'none', auto_resume:false}`, yet the DOM may
@@ -2660,7 +2693,9 @@ export class DeepSeekBrowser {
2660
2693
  // autoContinue is off. Do NOT wait out the whole timeout — the turn is
2661
2694
  // dead (truncated / finished with no answer), so tell ask() to RESEND
2662
2695
  // the prompt right away (that is the only remaining way to continue).
2663
- if (truncated || noAnswer) {
2696
+ // EXCEPT a head-only snapshot: the rest of the answer is still on
2697
+ // its way (see `headOnly`), so keep waiting for the append chunks.
2698
+ if ((truncated || noAnswer) && !headOnly) {
2664
2699
  throw new GenerationIncompleteError((this._netBody || 'finished without answer').slice(-300));
2665
2700
  }
2666
2701
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
@@ -257,13 +257,16 @@ export function isFinishedWithoutAnswer(body) {
257
257
  const finished = FINISHED_STATUS_RE.test(b);
258
258
  if (!finished)
259
259
  return false;
260
- // A RESPONSE fragment with real content means the model DID answer.
261
- const len = responseContentLength(b);
262
- if (len >= MIN_REAL_ANSWER_CHARS)
263
- return false;
264
- // No RESPONSE fragment at all (len === -1) or only a truncated head:
265
- // treat the absence of a real answer as "no answer produced".
266
- return true;
260
+ // MEASURED live: `responseContentLength` only sees the SNAPSHOT fragment
261
+ // (`v.response.fragments[]`), while DeepSeek appends the REAL answer through
262
+ // bare `data: {"v":...}` chunks. A live body with a `\n\n` snapshot and
263
+ // 1121 chars of appended text measured 2 chars, so 18 of 71 healthy bodies
264
+ // were classified as "finished WITHOUT an answer" — the finish loop then
265
+ // threw `GenerationIncompleteError` (no Continue button), ask() resent the
266
+ // task and the operator saw "DeepSeek оборвал ответ" on a perfectly good
267
+ // turn. The DECODED answer is the truth: use the real extractor.
268
+ const text = extractAnswer(b).trim();
269
+ return text === '' || TRUNCATED_HEAD_RE.test(text);
267
270
  }
268
271
  /**
269
272
  * True when a FINISHED body still carries NO complete answer — its only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.95.1",
3
+ "version": "2.95.3",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "start": "node ./dist/index.js",
23
- "dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev --headed",
23
+ "dev": "ZAMES_NET_DEBUG=1 ZAMES_ASK_DEBUG=1 tsx ./src/index.ts --dev --headed",
24
24
  "clean": "tsx scripts/clean-dist.mts",
25
25
  "build": "tsx scripts/clean-dist.mts && tsc -p tsconfig.build.json",
26
26
  "coverage:gate": "tsx scripts/coverage-gate.mts",