zames_pro 2.9.1 → 2.9.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.
@@ -80,6 +80,8 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
80
80
  // chatty model.
81
81
  let looksDoneRetries = 0;
82
82
  const MAX_LOOKSDONE_RETRIES = 3;
83
+ let plainTextRetries = 0;
84
+ const MAX_PLAINTEXT_RETRIES = 5;
83
85
  // Watchdog against the agent emitting a tool call and then going silent.
84
86
  // After a tool result the expected next answer is a fresh tool call; if we
85
87
  // instead get an EMPTY answer or the EXACT same answer as the previous turn
@@ -171,7 +173,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
171
173
  trimmed.length < 2 ||
172
174
  /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
173
175
  (trimmed.length <= 200 &&
174
- /(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed));
176
+ /(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
175
177
  if (looksService && stallRetries < MAX_STALL_RETRIES) {
176
178
  stallRetries++;
177
179
  transcript?.log('stall_retry', {
@@ -217,16 +219,25 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
217
219
  'сообщением оператору.';
218
220
  continue;
219
221
  }
220
- // All re-ask attempts are exhausted, yet the answer still looks like a
221
- // tool call. Most likely this is a silent stall: we show the operator a
222
- // warning in the terminal (not only in the transcript) so they see the
223
- // problem immediately instead of wondering why the agent stalled.
222
+ // STRICT: only tool calls and respond reach the operator. Plain text is
223
+ // a protocol violation: re-ask for a tool call instead of printing it.
224
+ if (plainTextRetries < MAX_PLAINTEXT_RETRIES) {
225
+ plainTextRetries++;
226
+ transcript?.log('plaintext_retry', {
227
+ attempt: plainTextRetries,
228
+ response: rawResponse.slice(0, 500),
229
+ });
230
+ message =
231
+ 'Only call tools. Do not write plain text. ' +
232
+ 'If the task is done - call respond with the final message. ' +
233
+ 'Otherwise reply with EXACTLY one JSON tool-call object, no text around it.';
234
+ continue;
235
+ }
224
236
  if (responseLooksLikeToolCall(rawResponse)) {
225
237
  transcript?.log('suspicious_final', { response: rawResponse });
226
- onWarning(translate(locale)('msg.suspicious_stop'));
227
238
  }
228
- onAssistantMessage(rawResponse);
229
- transcript?.log('assistant_final', { message: rawResponse });
239
+ transcript?.log('plaintext_final', { message: rawResponse });
240
+ onWarning(translate(locale)('msg.suspicious_stop'));
230
241
  return rawResponse;
231
242
  }
232
243
  const calls = Array.isArray(parsed) ? parsed : [parsed];
@@ -2,6 +2,8 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { execFileSync } from 'child_process';
5
+ import { extractPathToken } from './path-token.js';
6
+ export { extractPathToken };
5
7
  export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
6
8
  export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
7
9
  function sanitize(name) {
@@ -182,6 +184,15 @@ export async function saveToTemp(tmpDir, name, data) {
182
184
  await fs.writeFile(target, data);
183
185
  return target;
184
186
  }
187
+ // True if any OS clipboard tool is available on this machine.
188
+ export function hasClipboardTool() {
189
+ if (process.platform === 'win32' || process.platform === 'darwin')
190
+ return true;
191
+ if (process.platform === 'linux') {
192
+ return Boolean(tryCommand('xclip', ['-version']) || tryCommand('xsel', ['--version']) || tryCommand('wl-paste', ['--version']));
193
+ }
194
+ return false;
195
+ }
185
196
  // Read an image from the OS clipboard. Tries, in order, every known tool for
186
197
  // the current platform. Returns the raw bytes plus a short note about which
187
198
  // tool was used (or the reason nothing was found) — the caller shows that note
@@ -189,18 +200,33 @@ export async function saveToTemp(tmpDir, name, data) {
189
200
  // True when running inside WSL. There the X11 clipboard is a SEPARATE
190
201
  // clipboard from the Windows one: the user copies an image in Windows, so we
191
202
  // must read it through powershell.exe, not xclip.
203
+ export function windowsReachable() {
204
+ if (process.platform === 'win32')
205
+ return true;
206
+ if (process.platform !== 'linux')
207
+ return false;
208
+ try {
209
+ return require('fs').existsSync('/mnt/c/Windows');
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
192
215
  export function isWsl() {
193
216
  if (process.platform !== 'linux')
194
217
  return false;
218
+ let kernelWsl = false;
195
219
  if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
196
- return true;
197
- try {
198
- const rel = os.release().toLowerCase();
199
- if (rel.includes('microsoft') || rel.includes('wsl'))
200
- return true;
220
+ kernelWsl = true;
221
+ if (!kernelWsl) {
222
+ try {
223
+ const rel = os.release().toLowerCase();
224
+ if (rel.includes('microsoft') || rel.includes('wsl'))
225
+ kernelWsl = true;
226
+ }
227
+ catch { }
201
228
  }
202
- catch { }
203
- return false;
229
+ return kernelWsl && windowsReachable();
204
230
  }
205
231
  // PowerShell image grabber, embedded as a UTF-16LE base64 script and run
206
232
  // via powershell.exe -EncodedCommand: it saves the clipboard image to a
package/dist/browser.js CHANGED
@@ -40,9 +40,20 @@ const STOP_SELECTORS = [
40
40
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
41
41
  // DeepSeek's answer when the rate limit is exceeded.
42
42
  const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
43
+ // Transient server-side hiccups (DeepSeek overloaded / hiccup). Unlike the
44
+ // rate limit, these usually clear in seconds, so we retry quickly instead of
45
+ // waiting minutes. "Server busy", 503, "temporarily unavailable", etc.
46
+ const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
43
47
  export function isRateLimitText(text) {
44
48
  return RATE_LIMIT_RE.test(String(text || ''));
45
49
  }
50
+ export function isServerBusyText(text) {
51
+ const t = String(text || '');
52
+ // Rate limit takes priority (it needs the long wait).
53
+ if (RATE_LIMIT_RE.test(t))
54
+ return false;
55
+ return SERVER_BUSY_RE.test(t);
56
+ }
46
57
  // Normalize text for comparison: collapse whitespace so that tiny DOM
47
58
  // differences (nbsp, trailing spaces) don't count as "a new answer".
48
59
  export function normText(s) {
@@ -59,6 +70,13 @@ export class RateLimitError extends Error {
59
70
  this.name = 'RateLimitError';
60
71
  }
61
72
  }
73
+ // DeepSeek transient server error (Server busy).
74
+ export class ServerBusyError extends Error {
75
+ constructor(detail) {
76
+ super('Server busy. Try again later. ' + detail);
77
+ this.name = 'ServerBusyError';
78
+ }
79
+ }
62
80
  // ---------- profile cleanup ----------
63
81
  async function killStaleChrome() {
64
82
  if (process.platform !== 'win32')
@@ -105,6 +123,8 @@ export class DeepSeekBrowser {
105
123
  minSendIntervalMs;
106
124
  rateLimitWaitMs;
107
125
  maxRateLimitRetries;
126
+ maxServerBusyRetries;
127
+ serverBusyWaitMs;
108
128
  _lastSentAt;
109
129
  _abort;
110
130
  // The user pressed Esc/Ctrl+C — a "stop" for the WHOLE current batch of
@@ -119,7 +139,7 @@ export class DeepSeekBrowser {
119
139
  _netSniff;
120
140
  _netSniffLimit;
121
141
  _netHookInstalled;
122
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
142
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, } = {}) {
123
143
  this.headless = headless;
124
144
  this.debug = debug;
125
145
  this.channel = channel;
@@ -130,6 +150,8 @@ export class DeepSeekBrowser {
130
150
  this.minSendIntervalMs = minSendIntervalMs;
131
151
  this.rateLimitWaitMs = rateLimitWaitMs;
132
152
  this.maxRateLimitRetries = maxRateLimitRetries;
153
+ this.maxServerBusyRetries = maxServerBusyRetries;
154
+ this.serverBusyWaitMs = serverBusyWaitMs;
133
155
  this._lastSentAt = 0;
134
156
  this._abort = false;
135
157
  this._stopped = false;
@@ -420,6 +442,7 @@ export class DeepSeekBrowser {
420
442
  let lastErr = null;
421
443
  let attempt = 0;
422
444
  let rateLimitRetries = 0;
445
+ let serverBusyRetries = 0;
423
446
  while (attempt < this.askRetries) {
424
447
  attempt++;
425
448
  try {
@@ -629,6 +652,9 @@ export class DeepSeekBrowser {
629
652
  const pageText = await this._readPageText();
630
653
  if (isRateLimitText(pageText)) {
631
654
  throw new RateLimitError(pageText.slice(0, 300));
655
+ if (isServerBusyText(pageText)) {
656
+ throw new ServerBusyError(pageText.slice(0, 300));
657
+ }
632
658
  }
633
659
  const cur = await this._readLastAnswerTextClean().catch(() => '');
634
660
  const bodyLen = await this.page
@@ -672,6 +698,9 @@ export class DeepSeekBrowser {
672
698
  const pageText = await this._readPageText();
673
699
  if (isRateLimitText(pageText)) {
674
700
  throw new RateLimitError(pageText.slice(0, 300));
701
+ if (isServerBusyText(pageText)) {
702
+ throw new ServerBusyError(pageText.slice(0, 300));
703
+ }
675
704
  }
676
705
  }
677
706
  const cur = await this._readLastAnswerTextClean().catch(() => '');
package/dist/i18n.js CHANGED
@@ -150,8 +150,12 @@ const CATALOG = {
150
150
  en: '⚠ No image found in the clipboard (tried: {via}).',
151
151
  },
152
152
  'msg.clip_hint': {
153
- ru: ' Установите утилиту клипборда: Linux/X11 — xclip или xsel, Wayland — wl-clipboard; Windows/macOS — работает из коробки. Либо вставьте путь к файлу.',
154
- en: ' Install a clipboard tool: Linux/X11 — xclip or xsel, Wayland — wl-clipboard; Windows/macOS — works out of the box. Or paste a file path.',
153
+ ru: ' Проще всего: вставьте путь к файлу (например tmp/pic.png) — файл приложится к сообщению. Для клипборда нужна утилита (Linux/X11 — xclip или xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
154
+ en: ' Easiest: paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message. Clipboard paste needs a tool (Linux/X11 — xclip or xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
155
+ },
156
+ 'msg.clip_container': {
157
+ ru: ' Похоже, вы в контейнере/Dev Container: клипборд Windows отсюда недоступен. Вставьте путь к файлу (например tmp/pic.png) — он приложится к сообщению.',
158
+ en: ' Looks like a container/Dev Container: the Windows clipboard is not reachable from here. Paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message.',
155
159
  },
156
160
  'msg.from_queue': { ru: '▶ Из очереди: ', en: '▶ From queue: ' },
157
161
  'msg.agent_error': { ru: '✖ Ошибка агента:', en: '✖ Agent error:' },
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { createTools } from './tools.js';
8
8
  import { runAgentLoop } from './agent-loop.js';
9
9
  import { createSpinner } from './spinner.js';
10
10
  import { LineEditor, expandPastes, pasteReplacement } from './input.js';
11
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } from './attachments.js';
11
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
12
12
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
13
13
  import { runConfigMenu } from './config-menu.js';
14
14
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
@@ -1021,7 +1021,9 @@ async function main() {
1021
1021
  clipboardWarned = true;
1022
1022
  ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
1023
1023
  String.fromCharCode(10) +
1024
- t('msg.clip_hint')));
1024
+ (process.platform === 'linux' && !hasClipboardTool()
1025
+ ? t('msg.clip_container')
1026
+ : t('msg.clip_hint'))));
1025
1027
  }
1026
1028
  return null;
1027
1029
  }
@@ -0,0 +1,71 @@
1
+ // Find a single file path inside arbitrary text (a pasted path surrounded by
2
+ // words, e.g. a quoted path followed by a question). Returns the cleaned path
3
+ // plus the text around it, or null. Implemented WITHOUT regular expressions so
4
+ // the source has no escaping pitfalls.
5
+ export function extractPathToken(text) {
6
+ const s = String(text || '');
7
+ const chars = Array.from(s);
8
+ const n = chars.length;
9
+ let i = 0;
10
+ while (i < n) {
11
+ const c = chars[i];
12
+ if (c === ' ' || c === ' ') {
13
+ i++;
14
+ continue;
15
+ }
16
+ let quote = '';
17
+ let start = i;
18
+ if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
19
+ quote = c;
20
+ start = i + 1;
21
+ i = start;
22
+ while (i < n && chars[i] !== quote)
23
+ i++;
24
+ const tok = chars.slice(start, i).join('');
25
+ if (looksLikeToken(tok)) {
26
+ const before = s.slice(0, start - 1);
27
+ const after = i < n ? s.slice(i + 1) : '';
28
+ return { path: tok, before, after };
29
+ }
30
+ if (i < n)
31
+ i++;
32
+ continue;
33
+ }
34
+ i = start;
35
+ while (i < n && chars[i] !== ' ' && chars[i] !== ' ')
36
+ i++;
37
+ const tok = chars.slice(start, i).join('');
38
+ const cleaned = stripPunct(tok);
39
+ if (looksLikeToken(cleaned)) {
40
+ const before = s.slice(0, start);
41
+ const after = s.slice(start + tok.length);
42
+ return { path: cleaned, before, after };
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ function stripPunct(tok) {
48
+ let t = tok;
49
+ while (t.length && ',.!?;:)]'.includes(t[t.length - 1]))
50
+ t = t.slice(0, -1);
51
+ while (t.length && '(['.includes(t[0]))
52
+ t = t.slice(1);
53
+ return t;
54
+ }
55
+ function looksLikeToken(tok) {
56
+ const t = String(tok || '');
57
+ if (!t || t.length > 500)
58
+ return false;
59
+ const dot = t.lastIndexOf('.');
60
+ if (dot <= 0 || dot >= t.length - 1)
61
+ return false;
62
+ const ext = t.slice(dot + 1);
63
+ if (ext.length > 8)
64
+ return false;
65
+ for (const ch of ext) {
66
+ const ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
67
+ if (!ok)
68
+ return false;
69
+ }
70
+ return true;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {