zames_pro 2.17.0 → 2.18.0

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/README.md CHANGED
@@ -147,6 +147,9 @@ Codex CLI:
147
147
  alwaysConfirm regex list).
148
148
  - /add-dir <path> — validate an extra directory (the sandbox is fixed at
149
149
  startup; relaunch with --dir to write there).
150
+ - /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
151
+ dialogue into the terminal, so the restored context is visible. Only the
152
+ last 20 messages are shown (`RESTORED_HISTORY_LIMIT`).
150
153
  - /review [focus] [--staged] — ask the agent to review uncommitted changes
151
154
  and report findings (no code changes).
152
155
 
@@ -129,6 +129,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
129
129
  let unparsedRetries = 0;
130
130
  const MAX_UNPARSED_RETRIES = 4;
131
131
  let finalRespondAsked = false;
132
+ // The operator is told ONCE that the agent is re-asking the model for a
133
+ // proper tool call (not on every retry — that would spam the terminal).
134
+ let toolRetryWarned = false;
132
135
  // Guard against "the agent stalled": DeepSeek sometimes sends a final text
133
136
  // that merely DESCRIBES the next tool call (or cuts the answer off
134
137
  // mid-word), and the agent silently finishes the task even though the work
@@ -207,9 +210,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
207
210
  attempt: afterToolRetries,
208
211
  error: e.message,
209
212
  });
210
- safeWarning('browser.ask() did not return an answer within ' +
211
- Math.round(askDeadlineMs / 1000) +
212
- 's — retrying the request.');
213
+ safeWarning(translate(locale)('ds.answer_timeout', {
214
+ sec: Math.round(askDeadlineMs / 1000),
215
+ }));
213
216
  if (afterToolRetries < MAX_AFTER_TOOL_RETRIES) {
214
217
  afterToolRetries++;
215
218
  await new Promise((r) => setTimeout(r, 1500));
@@ -218,8 +221,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
218
221
  transcript?.log('ask_timeout_exhausted', {
219
222
  message: 'ask() did not return an answer and the retry limit is exhausted',
220
223
  });
221
- safeWarning('browser.ask() stopped responding; retry limit exhausted, ' +
222
- 'stopping. No model answer — check the DeepSeek chat manually.');
224
+ safeWarning(translate(locale)('ds.answer_timeout_give_up'));
223
225
  return 'ask() watchdog: no model answer received';
224
226
  }
225
227
  await reportChat();
@@ -402,6 +404,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
402
404
  attempt: unparsedRetries,
403
405
  response: rawResponse.slice(0, 500),
404
406
  });
407
+ // Tell the operator WHY nothing is happening: the model wrote text
408
+ // instead of a tool call and the agent is asking it to continue. Only
409
+ // once per task, otherwise the retry budget spams the terminal.
410
+ if (!toolRetryWarned) {
411
+ toolRetryWarned = true;
412
+ safeWarning(translate(locale)('ds.tool_retry', {
413
+ attempt: unparsedRetries,
414
+ max: MAX_UNPARSED_RETRIES,
415
+ }));
416
+ }
405
417
  message =
406
418
  (justRanTool
407
419
  ? 'You stopped after a tool call and wrote plain text. '
package/dist/browser.js CHANGED
@@ -277,6 +277,13 @@ export class DeepSeekBrowser {
277
277
  _netCapture;
278
278
  _netCaptureAt;
279
279
  _netChatId;
280
+ // Authorization / PoW headers sniffed from DeepSeek's own API requests, so
281
+ // fetchChatMessages can replay them (a bare fetch does not get them).
282
+ _apiAuth;
283
+ _apiPow;
284
+ // Last error from fetchChatMessages ('' on success). Surfaced under --debug
285
+ // so a missing restored history can be diagnosed.
286
+ _lastHistoryError;
280
287
  _netSniff;
281
288
  _netSniffLimit;
282
289
  _netHookInstalled;
@@ -321,6 +328,9 @@ export class DeepSeekBrowser {
321
328
  this._netCapture = '';
322
329
  this._netCaptureAt = 0;
323
330
  this._netChatId = null;
331
+ this._apiAuth = '';
332
+ this._apiPow = '';
333
+ this._lastHistoryError = '';
324
334
  this._netSniff = [];
325
335
  this._netSniffLimit = 5;
326
336
  this._netHookInstalled = false;
@@ -424,6 +434,23 @@ export class DeepSeekBrowser {
424
434
  pg.on('response', (resp) => {
425
435
  void this._onResponse(resp).catch(() => { });
426
436
  });
437
+ // DeepSeek's own API calls carry an Authorization header (the app reads a
438
+ // token from its storage and sets it explicitly). A bare fetch from our
439
+ // code does NOT get it, so history_messages would answer 401/empty.
440
+ // Sniff the header off the real requests and replay it later.
441
+ pg.on('request', (req) => {
442
+ try {
443
+ const url = req.url();
444
+ if (!/deepseek\.com\/api\//i.test(url))
445
+ return;
446
+ const h = req.headers();
447
+ if (h['authorization'])
448
+ this._apiAuth = h['authorization'];
449
+ if (h['x-ds-pow-response'])
450
+ this._apiPow = h['x-ds-pow-response'];
451
+ }
452
+ catch { }
453
+ });
427
454
  }
428
455
  async _onResponse(resp) {
429
456
  try {
@@ -1017,10 +1044,17 @@ export class DeepSeekBrowser {
1017
1044
  if (e instanceof RateLimitError) {
1018
1045
  rateLimitRetries++;
1019
1046
  if (rateLimitRetries > this.maxRateLimitRetries) {
1020
- console.error(theme.error(`✖ DeepSeek не принял сообщение после ${rateLimitRetries} пауз по ${Math.ceil(this.rateLimitWaitMs / 60000)} мин.`));
1047
+ console.error(theme.error(this._t('ds.rate_limit_give_up', {
1048
+ attempt: rateLimitRetries,
1049
+ min: Math.ceil(this.rateLimitWaitMs / 60000),
1050
+ })));
1021
1051
  throw e;
1022
1052
  }
1023
- console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
1053
+ console.error(theme.warn(this._t('ds.rate_limit_wait', {
1054
+ min: Math.ceil(this.rateLimitWaitMs / 60000),
1055
+ attempt: rateLimitRetries,
1056
+ max: this.maxRateLimitRetries,
1057
+ })));
1024
1058
  // Interruptible: Esc/Ctrl+C must cancel this long wait too,
1025
1059
  // otherwise "stop" stays frozen for up to 5 minutes.
1026
1060
  const aborted = await this._sleepInterruptible(this.rateLimitWaitMs);
@@ -1028,15 +1062,44 @@ export class DeepSeekBrowser {
1028
1062
  return '(прервано пользователем)';
1029
1063
  continue;
1030
1064
  }
1031
- console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
1065
+ // Server busy / overloaded: clear in seconds, so we retry quickly
1066
+ // (unlike the rate limit). Without this branch the error fell into the
1067
+ // generic ask() retry and the operator saw a bare message with no
1068
+ // explanation of what DeepSeek is doing.
1069
+ if (e instanceof ServerBusyError) {
1070
+ serverBusyRetries++;
1071
+ if (serverBusyRetries > this.maxServerBusyRetries) {
1072
+ console.error(theme.error(this._t('ds.server_busy_give_up', {
1073
+ attempt: serverBusyRetries,
1074
+ })));
1075
+ throw e;
1076
+ }
1077
+ console.error(theme.warn(this._t('ds.server_busy_wait', {
1078
+ sec: Math.ceil(this.serverBusyWaitMs / 1000),
1079
+ attempt: serverBusyRetries,
1080
+ max: this.maxServerBusyRetries,
1081
+ })));
1082
+ const aborted = await this._sleepInterruptible(this.serverBusyWaitMs);
1083
+ if (aborted)
1084
+ return '(прервано пользователем)';
1085
+ continue;
1086
+ }
1087
+ console.error('\n' +
1088
+ theme.warn(this._t('ds.ask_retry', {
1089
+ attempt,
1090
+ max: this.askRetries,
1091
+ error: e.message,
1092
+ })));
1032
1093
  if (/closed|crash|Target page|browser/i.test(e.message)) {
1033
- console.error('⚠ перезапускаю браузер...');
1094
+ console.error(theme.warn(this._t('ds.ask_restart_browser')));
1034
1095
  try {
1035
1096
  await this.restart();
1036
1097
  await this.waitForLogin();
1037
1098
  }
1038
1099
  catch (re) {
1039
- console.error(`⚠ не удалось перезапустить: ${re.message}`);
1100
+ console.error(theme.warn(this._t('ds.ask_restart_failed', {
1101
+ error: re.message,
1102
+ })));
1040
1103
  }
1041
1104
  }
1042
1105
  if (attempt < this.askRetries) {
@@ -1044,7 +1107,10 @@ export class DeepSeekBrowser {
1044
1107
  }
1045
1108
  }
1046
1109
  }
1047
- throw new Error(`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`);
1110
+ throw new Error(this._t('ds.ask_failed', {
1111
+ max: this.askRetries,
1112
+ error: lastErr?.message || '',
1113
+ }));
1048
1114
  }
1049
1115
  async _setInputText(input, text) {
1050
1116
  const tag = await input.evaluate((el) => el.tagName.toLowerCase());
@@ -1100,7 +1166,10 @@ export class DeepSeekBrowser {
1100
1166
  return el.innerText || el.textContent || "";
1101
1167
  });
1102
1168
  if (norm(got2) !== norm(text)) {
1103
- throw new Error("Не удалось вставить текст в поле ввода DeepSeek целиком (вставлено " + norm(got2).length + " из " + norm(text).length + " символов). Сообщение не отправлено, чтобы не отправить обрезанный текст.");
1169
+ throw new Error(this._t('ds.input_partial', {
1170
+ got: norm(got2).length,
1171
+ want: norm(text).length,
1172
+ }));
1104
1173
  }
1105
1174
  }
1106
1175
  }
@@ -1250,7 +1319,7 @@ export class DeepSeekBrowser {
1250
1319
  return '(прервано пользователем)';
1251
1320
  const input = await this._findVisible(INPUT_SELECTORS, 10_000);
1252
1321
  if (!input) {
1253
- throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
1322
+ throw new Error(this._t('ds.input_missing'));
1254
1323
  }
1255
1324
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
1256
1325
  this._askDebug('SEND agent=' + agent + ' len=' + prompt.length + ' beforeLen=' + beforeText.length + ' beforeHead=' + JSON.stringify(beforeText.slice(0, 60)));
@@ -1411,8 +1480,7 @@ export class DeepSeekBrowser {
1411
1480
  }
1412
1481
  }
1413
1482
  if (!started) {
1414
- throw new Error('Ответ не начал генерироваться за 35с даже после повторной отправки. ' +
1415
- 'Проверьте чат DeepSeek вручную.');
1483
+ throw new Error(this._t('ds.send_no_start'));
1416
1484
  }
1417
1485
  // Wait until the answer stops changing. We check the "not generating"
1418
1486
  // condition via text growth, NOT via _isGenerating().
@@ -1482,12 +1550,11 @@ export class DeepSeekBrowser {
1482
1550
  return this._netCapture;
1483
1551
  }
1484
1552
  this._askDebug('THROW no-new-answer lastLen=' + last.length + ' beforeLen=' + beforeText.length);
1485
- throw new Error('Новый ответ не получен (на странице остался прежний текст). ' +
1486
- 'Возможно, сообщение не отправилось.');
1553
+ throw new Error(this._t('ds.send_no_new_answer'));
1487
1554
  }
1488
1555
  async dumpDom(filePath) {
1489
1556
  if (!this.page)
1490
- throw new Error('браузер не запущен');
1557
+ throw new Error(this._t('ds.not_launched'));
1491
1558
  const html = await this.page.content();
1492
1559
  await fs.writeFile(filePath, html, 'utf-8');
1493
1560
  const report = await this.page.evaluate((sels) => {
@@ -1581,9 +1648,220 @@ export class DeepSeekBrowser {
1581
1648
  return true;
1582
1649
  }
1583
1650
  catch (e) {
1584
- throw new Error(`Не удалось открыть чат ${id}: ${e.message}`);
1651
+ throw new Error(this._t('ds.open_chat_failed', { id, error: e.message }));
1652
+ }
1653
+ }
1654
+ // Fetch the WHOLE dialogue from DeepSeek's own history endpoint instead of
1655
+ // scraping the DOM. The rendered page only shows a part of the history (the
1656
+ // list is virtualized), while /api/v0/chat/history_messages returns every
1657
+ // message of the chat as JSON. The request is made INSIDE the page, so the
1658
+ // auth cookie and the CSRF/PoW handling are the browser's.
1659
+ //
1660
+ // Shape of data.biz_data.chat_messages[]: { role: 'USER'|'ASSISTANT',
1661
+ // fragments: [{ type: 'REQUEST'|'RESPONSE'|'THINK'|'FILE', content }] }.
1662
+ // We keep REQUEST (user) and RESPONSE (assistant); THINK (reasoning) is
1663
+ // skipped and FILE fragments carry no text.
1664
+ async fetchChatMessages(id) {
1665
+ this._lastHistoryError = '';
1666
+ if (!this.page || !id)
1667
+ return [];
1668
+ // The Authorization header is sniffed off DeepSeek's own API requests. On
1669
+ // a freshly opened chat that request may not have fired yet — wait a bit
1670
+ // (up to 3s) so the replay is authorized instead of answering 401/empty.
1671
+ for (let i = 0; i < 15 && !this._apiAuth; i++) {
1672
+ await this.page.waitForTimeout(200);
1673
+ }
1674
+ const auth = this._apiAuth;
1675
+ const pow = this._apiPow;
1676
+ const res = await this.page
1677
+ .evaluate(async (opts) => {
1678
+ try {
1679
+ const url = 'https://chat.deepseek.com/api/v0/chat/history_messages?chat_session_id=' +
1680
+ encodeURIComponent(opts.chatId);
1681
+ const headers = {
1682
+ accept: 'application/json',
1683
+ };
1684
+ if (opts.auth)
1685
+ headers['authorization'] = opts.auth;
1686
+ if (opts.pow)
1687
+ headers['x-ds-pow-response'] = opts.pow;
1688
+ const resp = await fetch(url, {
1689
+ credentials: 'include',
1690
+ headers,
1691
+ });
1692
+ if (!resp.ok)
1693
+ return { error: 'HTTP ' + resp.status, list: [] };
1694
+ const json = await resp.json();
1695
+ const messages = json && json.data && json.data.biz_data
1696
+ ? json.data.biz_data.chat_messages
1697
+ : null;
1698
+ if (!Array.isArray(messages)) {
1699
+ return { error: 'no chat_messages in response', list: [] };
1700
+ }
1701
+ const NL = String.fromCharCode(10);
1702
+ const out = [];
1703
+ for (const m of messages) {
1704
+ const role = m && m.role === 'ASSISTANT' ? 'assistant' : 'user';
1705
+ const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
1706
+ let text = '';
1707
+ for (const fr of (m && m.fragments) || []) {
1708
+ if (!fr || fr.type !== want)
1709
+ continue;
1710
+ if (typeof fr.content === 'string')
1711
+ text += fr.content;
1712
+ }
1713
+ text = text.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
1714
+ if (text)
1715
+ out.push({ role, text });
1716
+ }
1717
+ return { error: '', list: out };
1718
+ }
1719
+ catch (e) {
1720
+ return { error: 'fetch failed: ' + e.message, list: [] };
1721
+ }
1722
+ }, { chatId: id, auth, pow })
1723
+ .catch((e) => ({
1724
+ error: 'evaluate failed: ' + e.message,
1725
+ list: [],
1726
+ }));
1727
+ this._lastHistoryError = res?.error || '';
1728
+ const list = (res?.list || []);
1729
+ if (list.length || !res?.error)
1730
+ return list;
1731
+ // Fallback: Playwright's own request context (shares the browser cookies)
1732
+ // when the in-page fetch was blocked (CSP, CORS, a page error).
1733
+ this._lastHistoryError = res.error;
1734
+ try {
1735
+ const resp = await this.page.request.get('https://chat.deepseek.com/api/v0/chat/history_messages?chat_session_id=' +
1736
+ encodeURIComponent(id), { headers: { accept: 'application/json' } });
1737
+ if (!resp.ok())
1738
+ return list;
1739
+ const json = (await resp.json());
1740
+ const messages = json?.data?.biz_data?.chat_messages;
1741
+ if (!Array.isArray(messages))
1742
+ return list;
1743
+ const NL = String.fromCharCode(10);
1744
+ const out = [];
1745
+ for (const m of messages) {
1746
+ const role = m && m.role === 'ASSISTANT' ? 'assistant' : 'user';
1747
+ const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
1748
+ let text = '';
1749
+ for (const fr of (m && m.fragments) || []) {
1750
+ if (!fr || fr.type !== want)
1751
+ continue;
1752
+ if (typeof fr.content === 'string')
1753
+ text += fr.content;
1754
+ }
1755
+ text = text.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
1756
+ if (text)
1757
+ out.push({ role, text });
1758
+ }
1759
+ this._lastHistoryError = '';
1760
+ return out;
1761
+ }
1762
+ catch (e) {
1763
+ this._lastHistoryError = 'request failed: ' + e.message;
1764
+ return list;
1585
1765
  }
1586
1766
  }
1767
+ // Read the WHOLE visible dialogue of the currently open chat, top to
1768
+ // bottom. Used by /resume and /resume-id so the operator sees the restored
1769
+ // context in the terminal instead of just "Chat opened.".
1770
+ //
1771
+ // Best-effort DOM scraping: DeepSeek renders every message as a markdown
1772
+ // block (assistant) or a plain bubble (user). Role detection relies on the
1773
+ // ds-assistant-message-* class; anything that is not an assistant block is
1774
+ // treated as a user message. The model's reasoning (.ds-think-content) is
1775
+ // skipped, exactly like in _readLastAnswerText.
1776
+ async readChatMessages() {
1777
+ if (!this.page)
1778
+ return [];
1779
+ const raw = await this.page
1780
+ .evaluate(() => {
1781
+ const inThink = (e) => {
1782
+ let n = e;
1783
+ while (n) {
1784
+ const cls = (n.className || '').toString();
1785
+ if (/ds-think-content|thinking-content/i.test(cls))
1786
+ return true;
1787
+ n = n.parentElement;
1788
+ }
1789
+ return false;
1790
+ };
1791
+ const textOf = (e) => {
1792
+ const h = e;
1793
+ const t = h.innerText || h.textContent || '';
1794
+ const NL = String.fromCharCode(10);
1795
+ return t.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
1796
+ };
1797
+ // A message block is "assistant" when it contains an answer markdown
1798
+ // wrapper (ds-markdown / ds-assistant-message), otherwise it is a
1799
+ // user bubble. DeepSeek's class names drift between builds, so the
1800
+ // detection is content-based, not class-prefix-based.
1801
+ const looksAssistant = (e) => {
1802
+ const cls = (e.className || '').toString();
1803
+ if (/ds-assistant-message|assistant-message/i.test(cls))
1804
+ return true;
1805
+ if (e.querySelector('[class*="ds-assistant-message"]'))
1806
+ return true;
1807
+ // A user bubble has no rendered markdown; an answer does.
1808
+ if (e.querySelector('[class*="ds-markdown"]'))
1809
+ return true;
1810
+ return false;
1811
+ };
1812
+ // Message-level containers first (broad), then the answer wrappers.
1813
+ const containerSels = [
1814
+ '[data-message-id]',
1815
+ '[class*="ds-message"]',
1816
+ '[class*="chat-message"]',
1817
+ '[class*="message-item"]',
1818
+ '[class*="_message"]',
1819
+ ];
1820
+ let blocks = [];
1821
+ for (const s of containerSels) {
1822
+ const found = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e) && textOf(e).length > 0);
1823
+ if (found.length) {
1824
+ blocks = found;
1825
+ break;
1826
+ }
1827
+ }
1828
+ const out = [];
1829
+ if (blocks.length) {
1830
+ for (const b of blocks) {
1831
+ const t = textOf(b);
1832
+ if (!t)
1833
+ continue;
1834
+ out.push({
1835
+ role: looksAssistant(b) ? 'assistant' : 'user',
1836
+ text: t,
1837
+ });
1838
+ }
1839
+ if (out.length)
1840
+ return out;
1841
+ }
1842
+ // Last resort: assistant answers only (no user turns) — better than
1843
+ // nothing when no message container matched.
1844
+ const sels = [
1845
+ 'div.ds-assistant-message-main-content',
1846
+ 'div[class*="ds-assistant-message-main-content"]',
1847
+ 'div[class*="ds-markdown"]',
1848
+ ];
1849
+ for (const s of sels) {
1850
+ const list = Array.from(document.querySelectorAll(s)).filter((e) => !inThink(e));
1851
+ if (!list.length)
1852
+ continue;
1853
+ for (const el of list) {
1854
+ const t = textOf(el);
1855
+ if (t)
1856
+ out.push({ role: 'assistant', text: t });
1857
+ }
1858
+ break;
1859
+ }
1860
+ return out;
1861
+ })
1862
+ .catch(() => []);
1863
+ return raw;
1864
+ }
1587
1865
  async getCurrentChatId() {
1588
1866
  try {
1589
1867
  const url = this.page.url();
package/dist/commands.js CHANGED
@@ -186,6 +186,78 @@ export function resolveExtraDir(input, workdir) {
186
186
  }
187
187
  return { path: abs };
188
188
  }
189
+ /** How many characters of the dialogue are printed by default. */
190
+ export const RESTORED_HISTORY_LIMIT = 20;
191
+ /**
192
+ * Is this message worth showing to the operator as part of the dialogue?
193
+ *
194
+ * The chat history contains a lot of protocol noise that is meaningless in
195
+ * the terminal: the system-prompt (a huge REQUEST), tool results
196
+ * ("Tool result for ..."), the agent's raw tool-calls (JSON / DSML), the
197
+ * "You stopped after a tool result..." nudges, and the agent's <system> notes.
198
+ * We keep only real user turns and real assistant answers.
199
+ */
200
+ export function isDisplayableMessage(m) {
201
+ const text = String((m && m.text) || '').trim();
202
+ if (!text)
203
+ return false;
204
+ if (text.length > 100_000)
205
+ return false; // the system-prompt / a giant blob
206
+ if (m.role === 'user') {
207
+ // Tool output, the system-prompt and the corrective nudges are not the
208
+ // operator's words.
209
+ if (/^Tool result for /.test(text))
210
+ return false;
211
+ if (/^You are a coding agent running in a terminal/.test(text))
212
+ return false;
213
+ if (/^You stopped after a tool result/.test(text))
214
+ return false;
215
+ if (/^\[system\]/.test(text))
216
+ return false;
217
+ if (/^The user ran \//.test(text))
218
+ return false;
219
+ return true;
220
+ }
221
+ // Assistant: a tool-call (JSON or DSML) is not an answer to show. The
222
+ // DSML markers use FULL-WIDTH vertical bars (||DSML), so the regex must
223
+ // match them, not the ASCII pipe.
224
+ if (/DSML/i.test(text))
225
+ return false;
226
+ if (/^<system>/i.test(text))
227
+ return false;
228
+ if (/"tool"\s*:/.test(text.slice(0, 600))) {
229
+ return false;
230
+ }
231
+ if (/^\s*[\[{]/.test(text) && /"args"\s*:/.test(text.slice(0, 600))) {
232
+ return false;
233
+ }
234
+ return true;
235
+ }
236
+ /**
237
+ * Keep only the last `limit` messages and drop empties/service noise. The
238
+ * source (network history or DOM) may contain protocol messages; they are
239
+ * filtered out here so the terminal output stays readable.
240
+ */
241
+ export function trimRestoredMessages(messages, limit = RESTORED_HISTORY_LIMIT) {
242
+ const clean = (messages || []).filter((m) => m && typeof m.text === 'string' && isDisplayableMessage(m));
243
+ if (limit > 0 && clean.length > limit)
244
+ return clean.slice(clean.length - limit);
245
+ return clean;
246
+ }
247
+ /**
248
+ * Render the restored dialogue as plain text lines: "❯ ..." for the operator,
249
+ * "● ..." for the agent. Markdown rendering is done by the caller (it needs
250
+ * the terminal width); this helper is pure and unit-tested.
251
+ */
252
+ export function formatRestoredHistory(messages, opts = {}) {
253
+ const list = trimRestoredMessages(messages, opts.limit ?? RESTORED_HISTORY_LIMIT);
254
+ const out = [];
255
+ for (const m of list) {
256
+ const marker = m.role === 'user' ? '❯ ' : '● ';
257
+ out.push(marker + m.text.trim());
258
+ }
259
+ return out.join(NL + NL);
260
+ }
189
261
  // ---------- /review ----------
190
262
  export function buildReviewPrompt(focus, hasStaged = false) {
191
263
  const scope = hasStaged ? 'staged' : 'uncommitted';
package/dist/i18n.js CHANGED
@@ -209,6 +209,83 @@ const CATALOG = {
209
209
  'msg.reload_error': { ru: 'Ошибка reload:', en: 'Reload error:' },
210
210
  'msg.abort_gen_short': { ru: '⏹ Esc — прерываю генерацию...', en: '⏹ Esc — aborting generation...' },
211
211
  'msg.abort_ctrlc_short': { ru: '⏹ Ctrl+C — прерываю генерацию...', en: '⏹ Ctrl+C — aborting generation...' },
212
+ // ---------- DeepSeek-side problems (what is happening right now) ----------
213
+ // The operator must understand WHY the agent is waiting or stopped: a rate
214
+ // limit, a server hiccup, a refused send, a login problem. These strings are
215
+ // printed around the moment they happen (browser.ts / agent-loop.ts), not as
216
+ // a bare error at the end of the run.
217
+ 'ds.rate_limit_wait': {
218
+ ru: '⏳ DeepSeek: «слишком часто». Жду {min} мин ({attempt}/{max}) и повторю отправку...',
219
+ en: '⏳ DeepSeek: "too frequent". Waiting {min} min ({attempt}/{max}), then resending...',
220
+ },
221
+ 'ds.rate_limit_give_up': {
222
+ ru: '✖ DeepSeek не принял сообщение после {attempt} пауз по {min} мин. Подожди и попробуй снова.',
223
+ en: '✖ DeepSeek refused the message after {attempt} waits of {min} min. Wait and try again.',
224
+ },
225
+ 'ds.server_busy_wait': {
226
+ ru: '⏳ DeepSeek: сервер занят. Жду {sec}с ({attempt}/{max}) и повторю...',
227
+ en: '⏳ DeepSeek: server busy. Waiting {sec}s ({attempt}/{max}), then retrying...',
228
+ },
229
+ 'ds.server_busy_give_up': {
230
+ ru: '✖ DeepSeek: сервер не ответил после {attempt} повторов. Попробуй позже.',
231
+ en: '✖ DeepSeek: the server did not respond after {attempt} retries. Try again later.',
232
+ },
233
+ 'ds.ask_retry': {
234
+ ru: '⚠ Ответ не получен ({attempt}/{max}): {error} — повторяю...',
235
+ en: '⚠ No answer yet ({attempt}/{max}): {error} — retrying...',
236
+ },
237
+ 'ds.ask_restart_browser': {
238
+ ru: '⚠ Браузер потерял страницу — перезапускаю и вхожу заново...',
239
+ en: '⚠ The browser lost the page — restarting and signing in again...',
240
+ },
241
+ 'ds.ask_restart_failed': {
242
+ ru: '⚠ Не удалось перезапустить браузер: {error}',
243
+ en: '⚠ Could not restart the browser: {error}',
244
+ },
245
+ 'ds.ask_failed': {
246
+ ru: '✖ Не удалось получить ответ от DeepSeek после {max} попыток: {error}',
247
+ en: '✖ Could not get an answer from DeepSeek after {max} attempts: {error}',
248
+ },
249
+ 'ds.input_missing': {
250
+ ru: '✖ Не найдено поле ввода на странице DeepSeek. Запусти /debug-dom и поправь INPUT_SELECTORS.',
251
+ en: '✖ The DeepSeek input field was not found. Run /debug-dom and fix INPUT_SELECTORS.',
252
+ },
253
+ 'ds.input_partial': {
254
+ ru: '✖ Не удалось вставить текст в поле ввода целиком ({got} из {want} символов). Сообщение не отправлено, чтобы не отправить обрезанный текст. Попробуй ещё раз или разбей сообщение.',
255
+ en: '✖ Could not paste the whole text into the input ({got} of {want} chars). The message was not sent to avoid sending a truncated text. Retry or split the message.',
256
+ },
257
+ 'ds.send_no_start': {
258
+ ru: '✖ Ответ не начал генерироваться за 35с даже после повторной отправки. Проверь чат DeepSeek вручную (возможно, кнопка отправки не нажимается или сессия разлогинилась).',
259
+ en: '✖ The answer did not start generating within 35s even after resending. Check the DeepSeek chat manually (the send button may not be clickable or the session may have expired).',
260
+ },
261
+ 'ds.send_no_new_answer': {
262
+ ru: '✖ Новый ответ не получен — на странице остался прежний текст. Возможно, сообщение не отправилось. Проверь чат DeepSeek вручную.',
263
+ en: '✖ No new answer received — the page still shows the previous text. The message may not have been sent. Check the DeepSeek chat manually.',
264
+ },
265
+ 'ds.answer_timeout': {
266
+ ru: '⏳ DeepSeek не ответил за {sec}с — повторяю запрос...',
267
+ en: '⏳ DeepSeek did not answer within {sec}s — retrying the request...',
268
+ },
269
+ 'ds.answer_timeout_give_up': {
270
+ ru: '✖ DeepSeek перестал отвечать, лимит повторов исчерпан. Модель не дала ответа — проверь чат DeepSeek вручную.',
271
+ en: '✖ DeepSeek stopped responding, retry limit exhausted. The model gave no answer — check the DeepSeek chat manually.',
272
+ },
273
+ 'ds.tool_retry': {
274
+ ru: '⏳ Агент не распознал ответ модели — прошу продолжить ({attempt}/{max})...',
275
+ en: '⏳ The agent did not recognize the model answer — asking it to continue ({attempt}/{max})...',
276
+ },
277
+ 'ds.stalled': {
278
+ ru: '⚠ Агент остановился, не завершив задачу (модель перестала вызывать инструменты). Проверь чат DeepSeek — задача может быть не выполнена.',
279
+ en: '⚠ The agent stopped before finishing (the model stopped calling tools). Check the DeepSeek chat — the task may be incomplete.',
280
+ },
281
+ 'ds.not_launched': {
282
+ ru: '✖ Браузер не запущен.',
283
+ en: '✖ The browser is not launched.',
284
+ },
285
+ 'ds.open_chat_failed': {
286
+ ru: '✖ Не удалось открыть чат {id}: {error}',
287
+ en: '✖ Could not open chat {id}: {error}',
288
+ },
212
289
  // ---------- self-review ----------
213
290
  'self.review_failed': { ru: 'Самообзор провалился:', en: 'Self-review failed:' },
214
291
  'self.fix_usage': { ru: 'Использование: /self-fix <name> [фокус]', en: 'Usage: /self-fix <name> [focus]' },
@@ -238,6 +315,12 @@ const CATALOG = {
238
315
  'chats.prompt_will_resend': { ru: ' Системный промпт будет переслан на следующей задаче.\n', en: ' System prompt will be resent on the next task.\n' },
239
316
  'chats.current_id': { ru: 'Текущий chat id: {v}', en: 'Current chat id: {v}' },
240
317
  'chats.not_created': { ru: 'Чат ещё не создан.', en: 'No chat created yet.' },
318
+ 'chats.history_title': { ru: 'Диалог чата:', en: 'Chat dialogue:' },
319
+ 'chats.history_empty': { ru: 'Диалог пуст или не удалось прочитать сообщения.', en: 'The dialogue is empty or the messages could not be read.' },
320
+ 'chats.history_service_only': { ru: 'В этом чате нет пользовательских реплик — только служебные сообщения агента (вызовы инструментов).', en: 'This chat has no user turns — only the agent service messages (tool calls).' },
321
+ 'chats.history_truncated': { ru: '… показаны последние {n} сообщений.', en: '… showing the last {n} messages.' },
322
+ 'chats.history_you': { ru: 'Вы', en: 'You' },
323
+ 'chats.history_agent': { ru: 'Агент', en: 'Agent' },
241
324
  'sessions.dir': { ru: 'Папка сессий: {v}', en: 'Sessions dir: {v}' },
242
325
  'sessions.none': { ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.', en: 'No saved sessions. They appear after the first task/chat.' },
243
326
  'sessions.restore_hint': { ru: 'Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.', en: 'Restore: /resume-id <id> (full id) or /resume <n> after /chats.' },
package/dist/index.js CHANGED
@@ -16,10 +16,12 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
16
16
  import { Transcript } from './transcript.js';
17
17
  import { UndoStore } from './undo.js';
18
18
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
19
- import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, } from './commands.js';
19
+ import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, } from './commands.js';
20
+ import { renderMarkdown } from './markdown.js';
20
21
  import { closeWeb } from './web.js';
21
22
  import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const NL = String.fromCharCode(10);
23
25
  // True when a DeepSeek session/credentials were stored earlier (auth.json).
24
26
  // Used by /doctor to report that auto re-login is ready.
25
27
  function authMarkerExists() {
@@ -788,6 +790,68 @@ async function resolveWorkdir() {
788
790
  }
789
791
  return dir;
790
792
  }
793
+ // ---------- restored dialogue ----------
794
+ /**
795
+ * Print the dialogue of the just-opened chat into the terminal. Called by
796
+ * /resume, /resume-id and on startup when a previous session is restored, so
797
+ * the operator sees the context instead of a bare "Chat opened.".
798
+ *
799
+ * Best-effort: a DOM-scraping failure must never break the restore.
800
+ */
801
+ async function printRestoredHistory(browser, ui, chatId = null) {
802
+ // Prefer DeepSeek's own history endpoint (complete, unvirtualized); fall
803
+ // back to scraping the rendered DOM when the request fails.
804
+ let all = [];
805
+ let fetchCount = -1;
806
+ let domCount = -1;
807
+ if (chatId && browser.fetchChatMessages) {
808
+ all = await browser.fetchChatMessages(chatId).catch(() => []);
809
+ fetchCount = all.length;
810
+ }
811
+ if (!all.length && browser.readChatMessages) {
812
+ all = await browser.readChatMessages().catch(() => []);
813
+ domCount = all.length;
814
+ }
815
+ const messages = trimRestoredMessages(all);
816
+ // Always surface the diagnostic line when the history could not be turned
817
+ // into anything printable — otherwise "the dialogue is empty" is a dead end
818
+ // (was the fetch blocked? did the chat really have no user turns?).
819
+ if (!all.length || (debug && !messages.length)) {
820
+ console.log(theme.dim(`[history] chat=${chatId || '-'} fetch=${fetchCount} dom=${domCount} raw=${all.length} shown=${messages.length} auth=${browser._apiAuth ? 'yes' : 'no'} err=${browser._lastHistoryError || '-'}`));
821
+ }
822
+ if (!all.length) {
823
+ console.log(theme.system(t('chats.history_empty')));
824
+ return;
825
+ }
826
+ if (!messages.length) {
827
+ // The history exists but contains only service/protocol messages (e.g.
828
+ // a chat where only the system-prompt and tool-calls were stored).
829
+ console.log(theme.system(t('chats.history_service_only', { n: String(all.length) })));
830
+ return;
831
+ }
832
+ const out = (text) => {
833
+ if (ui)
834
+ ui.printAbove(text);
835
+ else
836
+ console.log(text);
837
+ };
838
+ out(theme.system(t('chats.history_title')));
839
+ for (const m of messages) {
840
+ if (m.role === 'user') {
841
+ out(theme.user('❯ ' + t('chats.history_you') + ': ') + m.text.trim());
842
+ }
843
+ else {
844
+ out(NL +
845
+ theme.assistant('● ' + t('chats.history_agent') + ':') +
846
+ NL +
847
+ renderMarkdown(m.text.trim()) +
848
+ NL);
849
+ }
850
+ }
851
+ if (all.length > messages.length) {
852
+ out(theme.dim(t('chats.history_truncated', { n: String(RESTORED_HISTORY_LIMIT) })));
853
+ }
854
+ }
791
855
  // ---------- task runner ----------
792
856
  async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
793
857
  const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
@@ -1091,6 +1155,7 @@ async function main() {
1091
1155
  sendSystemPromptNext = resendPrompt;
1092
1156
  saveLastChat(resumeId, currentWorkdir);
1093
1157
  console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
1158
+ await printRestoredHistory(browser, null, resumeId);
1094
1159
  }
1095
1160
  catch (e) {
1096
1161
  console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
@@ -1774,6 +1839,7 @@ async function main() {
1774
1839
  theme.system(resendPrompt
1775
1840
  ? ' Системный промпт будет переслан на следующей задаче.\n'
1776
1841
  : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1842
+ await printRestoredHistory(browser, editor, pick.id);
1777
1843
  }
1778
1844
  catch (e) {
1779
1845
  console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
@@ -1831,6 +1897,7 @@ async function main() {
1831
1897
  transcript.log('resume_chat', { id });
1832
1898
  console.log(theme.assistant('Чат открыт.') +
1833
1899
  theme.system(String.fromCharCode(10)));
1900
+ await printRestoredHistory(browser, editor, id);
1834
1901
  }
1835
1902
  catch (e) {
1836
1903
  console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "start": "node ./dist/index.js",
18
- "dev": "tsx ./src/index.ts --dev",
18
+ "dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev",
19
19
  "build": "tsc -p tsconfig.build.json",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "prepublishOnly": "npm run build",