zames_pro 2.29.4 → 2.29.6

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
@@ -1740,6 +1740,17 @@ export class DeepSeekBrowser {
1740
1740
  // code and the agent hung on "Stopped" until the timeout.
1741
1741
  if (this._netCaptureAt >= this._lastSentAt &&
1742
1742
  isGenerationIncompleteText(this._netBody)) {
1743
+ // PREFER the Continue button: it RESUMES the same truncated turn (a
1744
+ // `chat/continue` request). Resending the prompt creates a NEW turn and
1745
+ // duplicates the work. Only resend when there is no button to click.
1746
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1747
+ this._askDebug('CLICKED Continue (incomplete, start-loop)');
1748
+ this._netBody = '';
1749
+ this._netCaptureAt = 0;
1750
+ this._netNoAnswer = false;
1751
+ await this.page.waitForTimeout(500);
1752
+ continue;
1753
+ }
1743
1754
  throw new GenerationIncompleteError(this._netBody.slice(-300));
1744
1755
  }
1745
1756
  // A FINISHED turn with NO answer (only reasoning): the UI shows a
@@ -1837,7 +1848,20 @@ export class DeepSeekBrowser {
1837
1848
  // treating this as "send failed" and hammering Enter.
1838
1849
  if (this._netCaptureAt >= this._lastSentAt &&
1839
1850
  (isGenerationIncompleteText(this._netBody) || this._netNoAnswer)) {
1840
- throw new GenerationIncompleteError(this._netBody.slice(-300));
1851
+ // Prefer Continue (resumes the same turn); resend only if unavailable.
1852
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1853
+ this._askDebug('CLICKED Continue (incomplete, pre-loop)');
1854
+ this._netBody = '';
1855
+ this._netCaptureAt = 0;
1856
+ this._netNoAnswer = false;
1857
+ await this.page.waitForTimeout(500);
1858
+ started = true;
1859
+ // fall through to the finish loop by NOT returning here
1860
+ lastStartCur = cur;
1861
+ }
1862
+ else {
1863
+ throw new GenerationIncompleteError(this._netBody.slice(-300));
1864
+ }
1841
1865
  }
1842
1866
  if (cur &&
1843
1867
  cur.trim() &&
@@ -1905,30 +1929,32 @@ export class DeepSeekBrowser {
1905
1929
  throw new ServerBusyError(pageText.slice(0, 300));
1906
1930
  }
1907
1931
  }
1908
- // The server truncated the turn (generation_err / INCOMPLETE). The DOM
1909
- // keeps the partial answer, so without this check the loop waits out the
1910
- // whole timeout and throws ds.send_no_new_answer — the operator sees the
1911
- // agent "stop" with a Continue button in the chat. Retry the send.
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 &&
1922
- this._netCaptureAt >= this._lastSentAt &&
1923
- this.autoContinue) {
1932
+ // The server truncated the turn (generation_err / INCOMPLETE) or ended
1933
+ // it with only reasoning. PREFER the Continue button: it RESUMES the
1934
+ // same turn (`chat/continue`). Resending the prompt creates a NEW turn
1935
+ // and duplicates the work — only do that when there is no button to
1936
+ // click. Runs on the RAW `_netBody` (a truncated body has no RESPONSE,
1937
+ // so `_netCapture` is empty and the old guard was dead code).
1938
+ const truncated = this._netCaptureAt >= this._lastSentAt &&
1939
+ isGenerationIncompleteText(this._netBody);
1940
+ const noAnswer = this._netNoAnswer && this._netCaptureAt >= this._lastSentAt;
1941
+ if ((truncated || noAnswer) && this.autoContinue) {
1924
1942
  if (await this._clickContinueIfVisible()) {
1925
- this._askDebug('CLICKED Continue (net-no-answer, finish-loop)');
1943
+ this._askDebug('CLICKED Continue (finish-loop, truncated=' +
1944
+ truncated +
1945
+ ' noAnswer=' +
1946
+ noAnswer +
1947
+ ')');
1948
+ this._netBody = '';
1926
1949
  this._netNoAnswer = false;
1927
1950
  this._netCaptureAt = 0;
1928
1951
  await this.page.waitForTimeout(500);
1929
1952
  continue;
1930
1953
  }
1931
1954
  }
1955
+ if (truncated) {
1956
+ throw new GenerationIncompleteError(this._netBody.slice(-300));
1957
+ }
1932
1958
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
1933
1959
  // Continue button; the model does not resume by itself. Click it so the
1934
1960
  // answer keeps flowing instead of waiting for the operator.
package/dist/commands.js CHANGED
@@ -197,60 +197,115 @@ export function resolveExtraDir(input, workdir) {
197
197
  }
198
198
  return { path: abs };
199
199
  }
200
- /** How many characters of the dialogue are printed by default. */
200
+ /** How many messages of the dialogue are printed by default. */
201
201
  export const RESTORED_HISTORY_LIMIT = 20;
202
202
  /**
203
- * Is this message worth showing to the operator as part of the dialogue?
203
+ * DeepSeek stores the assistant turn VERBATIM: either a raw tool-call JSON
204
+ * ({"tool":"Read","args":{...}}, frequently dirty/truncated) or a `respond`
205
+ * call that carries the real message for the operator
206
+ * ({"tool":"respond","args":{"message":"..."}}). The operator only wants the
207
+ * `respond` message (and plain-text answers) — a tool-call is protocol noise.
204
208
  *
205
- * The chat history contains a lot of protocol noise that is meaningless in
206
- * the terminal: the system-prompt (a huge REQUEST), tool results
207
- * ("Tool result for ..."), the agent's raw tool-calls (JSON / DSML), the
208
- * "You stopped after a tool result..." nudges, and the agent's <system> notes.
209
- * We keep only real user turns and real assistant answers.
209
+ * Returns the tool name and, for `respond`, the unwrapped message. Tolerant on
210
+ * purpose: the stored JSON is often dirty (a stray `}}`, a missing "args"
211
+ * wrapper), so a bare JSON.parse is not enough.
210
212
  */
211
- export function isDisplayableMessage(m) {
213
+ export function parseAssistantToolCall(text) {
214
+ const t = String(text || '').trim();
215
+ if (!t)
216
+ return null;
217
+ // Broken heads like `<|tool": ...` or `tool": "Bash"` are tolerated.
218
+ const m = t.match(/tool"?\s*:\s*"([^"\s]+)"/);
219
+ if (!m)
220
+ return null;
221
+ const tool = m[1];
222
+ if (tool !== 'respond')
223
+ return { tool };
224
+ const greedy = t.match(/"message"\s*:\s*"([\s\S]*)"\s*\}*\s*$/);
225
+ let raw = greedy ? greedy[1] : '';
226
+ if (!greedy) {
227
+ const idx = t.indexOf('"message"');
228
+ if (idx >= 0)
229
+ raw = t.slice(idx + 9).replace(/^\s*:\s*"/, '');
230
+ }
231
+ return { tool, message: unescapeJsonString(raw) };
232
+ }
233
+ /** Decode the JSON escape sequences that appear inside a captured string. */
234
+ export function unescapeJsonString(s) {
235
+ return String(s || '')
236
+ .replace(/\\u([0-9a-fA-F]{4})/g, (_x, h) => String.fromCharCode(parseInt(h, 16)))
237
+ .replace(/\\n/g, NL)
238
+ .replace(/\\r/g, String.fromCharCode(13))
239
+ .replace(/\\t/g, String.fromCharCode(9))
240
+ .replace(/\\"/g, '"')
241
+ .replace(/\\\\/g, '\\');
242
+ }
243
+ /**
244
+ * Normalize one restored message for display:
245
+ * - USER: drop the protocol noise (system-prompt, tool results, nudges) — it
246
+ * is not the operator's words.
247
+ * - ASSISTANT: unwrap a `respond` call to its message; drop any other
248
+ * tool-call (protocol noise); keep plain-text answers.
249
+ * Returns null when the message must not be shown.
250
+ */
251
+ export function normalizeRestoredMessage(m) {
212
252
  const text = String((m && m.text) || '').trim();
213
253
  if (!text)
214
- return false;
254
+ return null;
215
255
  if (text.length > 100_000)
216
- return false; // the system-prompt / a giant blob
256
+ return null; // the system-prompt / a giant blob
217
257
  if (m.role === 'user') {
218
- // Tool output, the system-prompt and the corrective nudges are not the
219
- // operator's words.
220
258
  if (/^Tool result for /.test(text))
221
- return false;
259
+ return null;
222
260
  if (/^You are a coding agent running in a terminal/.test(text))
223
- return false;
261
+ return null;
224
262
  if (/^You stopped after a tool result/.test(text))
225
- return false;
263
+ return null;
226
264
  if (/^\[system\]/.test(text))
227
- return false;
265
+ return null;
228
266
  if (/^The user ran \//.test(text))
229
- return false;
230
- return true;
267
+ return null;
268
+ return { role: 'user', text };
231
269
  }
232
- // Assistant: a tool-call (JSON or DSML) is not an answer to show. The
233
- // DSML markers use FULL-WIDTH vertical bars (||DSML), so the regex must
234
- // match them, not the ASCII pipe.
235
270
  if (/DSML/i.test(text))
236
- return false;
271
+ return null;
237
272
  if (/^<system>/i.test(text))
238
- return false;
239
- if (/"tool"\s*:/.test(text.slice(0, 600))) {
240
- return false;
273
+ return null;
274
+ // The model sometimes emits its own safety-classification block as a plain
275
+ // "answer"; it is not for the operator.
276
+ if (/<ds_safety>/i.test(text))
277
+ return null;
278
+ if (/^Safe$/i.test(text))
279
+ return null;
280
+ const call = parseAssistantToolCall(text);
281
+ if (call) {
282
+ if (call.tool !== 'respond')
283
+ return null;
284
+ const msg = (call.message || '').trim();
285
+ if (!msg)
286
+ return null;
287
+ return { role: 'assistant', text: msg };
241
288
  }
242
- if (/^\s*[\[{]/.test(text) && /"args"\s*:/.test(text.slice(0, 600))) {
243
- return false;
244
- }
245
- return true;
289
+ // A plain-text answer (written without the respond wrapper) — show as-is.
290
+ return { role: 'assistant', text };
291
+ }
292
+ /** Back-compat predicate: is the message worth showing at all? */
293
+ export function isDisplayableMessage(m) {
294
+ return normalizeRestoredMessage(m) !== null;
246
295
  }
247
296
  /**
248
- * Keep only the last `limit` messages and drop empties/service noise. The
249
- * source (network history or DOM) may contain protocol messages; they are
250
- * filtered out here so the terminal output stays readable.
297
+ * Keep only the last `limit` messages, dropping protocol noise and UNWRAPPING
298
+ * the assistant `respond` calls to their operator-facing text.
251
299
  */
252
300
  export function trimRestoredMessages(messages, limit = RESTORED_HISTORY_LIMIT) {
253
- const clean = (messages || []).filter((m) => m && typeof m.text === 'string' && isDisplayableMessage(m));
301
+ const clean = [];
302
+ for (const m of messages || []) {
303
+ if (!m || typeof m.text !== 'string')
304
+ continue;
305
+ const n = normalizeRestoredMessage(m);
306
+ if (n)
307
+ clean.push(n);
308
+ }
254
309
  if (limit > 0 && clean.length > limit)
255
310
  return clean.slice(clean.length - limit);
256
311
  return clean;
package/dist/i18n.js CHANGED
@@ -534,8 +534,8 @@ const CATALOG = {
534
534
  en: '⚠ No answer yet ({attempt}/{max}): {error} — retrying...',
535
535
  },
536
536
  'ds.incomplete_retry': {
537
- ru: '⏳ DeepSeek оборвал ответ (кнопка Continue в чате) — повторяю отправку ({attempt}/{max})...',
538
- en: '⏳ DeepSeek truncated the answer (the Continue button in the chat) — resending ({attempt}/{max})...',
537
+ ru: '⏳ DeepSeek оборвал ответ — жму «Continue», чтобы продолжить (повтор {attempt}/{max})...',
538
+ en: '⏳ DeepSeek truncated the answer — clicking Continue to resume (retry {attempt}/{max})...',
539
539
  },
540
540
  'ds.incomplete_give_up': {
541
541
  ru: '✖ DeepSeek {attempt} раз подряд оборвал ответ, не дойдя до конца. Возможно, включён режим размышления — попробуй выключить его (browser.deepThinking) или повтори позже.',
package/dist/index.js CHANGED
@@ -828,6 +828,12 @@ async function printRestoredHistory(browser, ui, chatId = null) {
828
828
  all = await browser.readChatMessages().catch(() => []);
829
829
  domCount = all.length;
830
830
  }
831
+ // `all` is the raw history (mostly protocol noise). `displayable` is what
832
+ // survives normalization; `messages` is the last RESTORED_HISTORY_LIMIT of
833
+ // those. The "truncated" note must compare the DISPLAYABLE count with the
834
+ // limit (comparing `all.length` always fired, because tool-calls are
835
+ // dropped and the raw history is always longer).
836
+ const displayable = trimRestoredMessages(all, 0);
831
837
  const messages = trimRestoredMessages(all);
832
838
  // Always surface the diagnostic line when the history could not be turned
833
839
  // into anything printable — otherwise "the dialogue is empty" is a dead end
@@ -868,7 +874,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
868
874
  NL);
869
875
  }
870
876
  }
871
- if (all.length > messages.length) {
877
+ if (displayable.length > messages.length) {
872
878
  out(theme.dim(t('chats.history_truncated', { n: String(RESTORED_HISTORY_LIMIT) })));
873
879
  }
874
880
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.29.4",
3
+ "version": "2.29.6",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {