zames_pro 2.82.49 → 2.82.51

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +159 -26
  3. package/SECURITY.md +50 -0
  4. package/dist/agent-loop.js +35 -5
  5. package/dist/answer-parse.js +1 -1
  6. package/dist/attach-flow.js +7 -3
  7. package/dist/attach-refs.js +30 -6
  8. package/dist/attachments.js +9 -0
  9. package/dist/browser-chats.js +9 -2
  10. package/dist/browser-dom.js +1 -1
  11. package/dist/browser-pure.js +9 -3
  12. package/dist/browser.js +1 -1
  13. package/dist/changelog.js +1 -1
  14. package/dist/checkpoint.js +1 -1
  15. package/dist/cli-args.js +1 -1
  16. package/dist/config-command.js +1 -1
  17. package/dist/config-commands.js +5 -2
  18. package/dist/config-live.js +10 -0
  19. package/dist/config.js +13 -1
  20. package/dist/context-command.js +6 -0
  21. package/dist/context.js +27 -4
  22. package/dist/diff.js +52 -9
  23. package/dist/extraTools.js +7 -0
  24. package/dist/glm-chats.js +18 -1
  25. package/dist/glm-net-capture.js +23 -5
  26. package/dist/goal.js +6 -1
  27. package/dist/i18n/deepseek.js +3 -3
  28. package/dist/i18n/self-review.js +4 -0
  29. package/dist/i18n.js +8 -2
  30. package/dist/index.js +90 -101
  31. package/dist/input/layout.js +33 -8
  32. package/dist/input/suggest.js +1 -1
  33. package/dist/input.js +1 -1
  34. package/dist/limits-command.js +42 -0
  35. package/dist/mcp.js +23 -4
  36. package/dist/model-command.js +21 -0
  37. package/dist/path-token.js +36 -9
  38. package/dist/permissions.js +32 -3
  39. package/dist/provider-command.js +29 -0
  40. package/dist/provider-handoff.js +18 -1
  41. package/dist/queue-command.js +15 -0
  42. package/dist/qwen-chats.js +13 -6
  43. package/dist/qwen-net-capture.js +13 -6
  44. package/dist/resume-command.js +38 -0
  45. package/dist/send-telemetry.js +15 -3
  46. package/dist/send-throttle.js +3 -1
  47. package/dist/status-command.js +68 -0
  48. package/dist/subagent.js +5 -12
  49. package/dist/tool-summary.js +8 -1
  50. package/dist/tools.js +13 -1
  51. package/dist/tui/StatusBar.js +1 -1
  52. package/dist/tui/log-window.js +41 -4
  53. package/dist/undo.js +11 -1
  54. package/dist/web.js +16 -6
  55. package/llms-full.txt +679 -0
  56. package/llms.txt +102 -0
  57. package/package.json +84 -23
@@ -14,7 +14,7 @@ export function cleanAnswer(raw) {
14
14
  return '';
15
15
  return raw;
16
16
  }
17
- // Pure, state-free helpers extracted from browser.ts (BACKLOG C3). Nothing
17
+ // Pure, state-free helpers extracted from browser.ts . Nothing
18
18
  // here touches the DeepSeekBrowser instance state, so this module is safe to
19
19
  // import anywhere (including hot-reloaded modules) without pulling in the
20
20
  // live Playwright context. browser.ts re-exports the public names, so the
@@ -245,8 +245,14 @@ export function mergeAuthMarker(raw, provider, at) {
245
245
  providers[k] = v || {};
246
246
  }
247
247
  else if (j && typeof j === 'object') {
248
- if (j.provider === 'qwen')
249
- providers.qwen = { at: j.at };
248
+ // A legacy record carries ONE provider name. Only Qwen and GLM ever wrote
249
+ // one (`{provider:'qwen'|'glm', at}`); a BARE `{at}` was DeepSeek. Treating
250
+ // every non-qwen name as DeepSeek filed a legacy GLM session under
251
+ // `deepseek`, so /doctor reported a DeepSeek session and the GLM one
252
+ // vanished.
253
+ const p = typeof j.provider === 'string' ? j.provider : '';
254
+ if (p === 'qwen' || p === 'glm')
255
+ providers[p] = { at: j.at };
250
256
  else if (typeof j.at === 'string')
251
257
  providers.deepseek = { at: j.at };
252
258
  }
package/dist/browser.js CHANGED
@@ -18,7 +18,7 @@ import { translate, DEFAULT_LOCALE } from './i18n.js';
18
18
  // here preserves the existing public API (test/*.test.ts import them from
19
19
  // browser.ts).
20
20
  export { STOP_NAME_RE, CONTINUE_NAME_RE };
21
- // The pure, state-free helpers live in browser-pure.ts (BACKLOG C3): they are
21
+ // The pure, state-free helpers live in browser-pure.ts : they are
22
22
  // imported for the class below and RE-EXPORTED here, so the public API of this
23
23
  // module is unchanged (tests and other modules import these names from
24
24
  // './browser.js').
package/dist/changelog.js CHANGED
@@ -1,4 +1,4 @@
1
- // Conventional-Commits -> CHANGELOG draft (BACKLOG D2). PURE and
1
+ // Conventional-Commits -> CHANGELOG draft. PURE and
2
2
  // unit-tested: the draft is built from git log OUTSIDE the release flow
3
3
  // (scripts/changelog-draft.mts), so a misparse never touches CHANGELOG.md.
4
4
  // We do NOT auto-write the file: a human (or the agent) reviews the draft
@@ -4,7 +4,7 @@ import os from 'os';
4
4
  import { execFile } from 'child_process';
5
5
  import { randomBytes } from 'crypto';
6
6
  import { writeJsonAtomic } from './fsutil.js';
7
- // Checkpoints / rewind (BACKLOG B3).
7
+ // Checkpoints / rewind .
8
8
  //
9
9
  // Undo (src/undo.ts) reverts ONE file write. A checkpoint snapshots the WHOLE
10
10
  // working tree at the start of a task, so a risky task can be rolled back with
package/dist/cli-args.js CHANGED
@@ -1,4 +1,4 @@
1
- // PURE CLI argument parsing (BACKLOG C3). index.ts used to hold `args` as a
1
+ // PURE CLI argument parsing . index.ts used to hold `args` as a
2
2
  // module-level constant plus three helpers closing over it, which made the
3
3
  // parsing impossible to unit-test in isolation (index.ts is the entry point and
4
4
  // is not covered). `createCliArgs(argv)` binds the argv ONCE and returns the
@@ -1,4 +1,4 @@
1
- // The /config subcommand DISPATCH (BACKLOG C3). The PURE text rendering lives
1
+ // The /config subcommand DISPATCH . The PURE text rendering lives
2
2
  // in config-commands.ts; this module owns the parsing/routing of the text
3
3
  // subcommands (list/get/set/reset/lang/path) and prints through injected
4
4
  // callbacks. Everything that touches live state (the runtime config object,
@@ -1,4 +1,4 @@
1
- // PURE rendering for the /config command's TEXT output (BACKLOG C3). These
1
+ // PURE rendering for the /config command's TEXT output . These
2
2
  // helpers format the schema into lines; they touch no browser/terminal state,
3
3
  // so they are unit-tested without a live agent (the /config dispatch in
4
4
  // index.ts only prints what these return). The interactive menu lives in
@@ -14,7 +14,10 @@ export function configListValue(field, value, t) {
14
14
  return t('cfg.menu.default');
15
15
  if (typeof value === 'boolean')
16
16
  return value ? t('common.on') : t('common.off');
17
- if (/password/i.test(field.path) && String(value).length > 0)
17
+ // Mask a real secret, but NOT a whitespace-only one — the same rule as
18
+ // configGetValue(): a blank password cannot log in, and masking it makes
19
+ // the list claim a password is configured while auto-login silently fails.
20
+ if (/password/i.test(field.path) && String(value).trim().length > 0)
18
21
  return '********';
19
22
  return JSON.stringify(value);
20
23
  }
@@ -31,6 +31,16 @@ export function applyBrowserLiveConfig(path, value, browser) {
31
31
  if (!field || !(field in browser))
32
32
  return false;
33
33
  browser[field] = value;
34
+ // `answerTimeoutMs` is DERIVED into the watchdog (`askDeadlineMs` = the
35
+ // answer timeout + 60s) by the Qwen/GLM constructors. Pushing only the field
36
+ // left the deadline at its old value, so a LARGER timeout made the agent
37
+ // give up before the browser's own timeout ever fired. Re-derive it the same
38
+ // way the constructor does.
39
+ if (field === 'answerTimeoutMs' && 'askDeadlineMs' in browser) {
40
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
41
+ browser.askDeadlineMs = value + 60_000;
42
+ }
43
+ }
34
44
  return true;
35
45
  }
36
46
  /** Push undo.* / checkpoint.* changes onto the live stores. The stores copy
package/dist/config.js CHANGED
@@ -70,7 +70,7 @@ export const DEFAULTS = {
70
70
  enabled: true,
71
71
  maxBackups: 200,
72
72
  },
73
- // BACKLOG B3: snapshot the whole working tree at the start of each task so
73
+ // Snapshot the whole working tree at the start of each task so
74
74
  // /rewind can roll a risky task back in one step.
75
75
  checkpoint: {
76
76
  enabled: true,
@@ -601,11 +601,23 @@ export function getConfigField(path) {
601
601
  }
602
602
  export function getByPath(obj, path) {
603
603
  const parts = path.split('.');
604
+ // Same guard as setByPath: a dotted path must never reach the PROTOTYPE
605
+ // chain. `__proto__` used to return Object.prototype (an object), and
606
+ // `constructor` / `toString` the corresponding FUNCTION — the /config list
607
+ // then rendered `{}` for a field whose path did not exist instead of the
608
+ // default marker. Reading those keys is never meaningful for a config path.
609
+ // `toString`, `valueOf`, `hasOwnProperty`, ... are inherited METHODS, not
610
+ // config values: reading them through a dotted path returned a FUNCTION.
611
+ // Only an OWN property counts.
612
+ if (parts.some((p) => UNSAFE_PATH_KEYS.has(p)))
613
+ return undefined;
604
614
  let cur = obj;
605
615
  for (const p of parts) {
606
616
  if (cur === null || cur === undefined || typeof cur !== 'object') {
607
617
  return undefined;
608
618
  }
619
+ if (!Object.prototype.hasOwnProperty.call(cur, p))
620
+ return undefined;
609
621
  cur = cur[p];
610
622
  }
611
623
  return cur;
@@ -0,0 +1,6 @@
1
+ import { formatContextSources } from './commands.js';
2
+ import { translate } from './i18n.js';
3
+ /** Render the `/context` report for already-loaded sources. */
4
+ export function renderContextReport(src, opts = {}, t = translate('en')) {
5
+ return formatContextSources(src.agents || [], src.memory || [], src.skills || [], src.commands || [], opts, t);
6
+ }
package/dist/context.js CHANGED
@@ -186,9 +186,25 @@ function clipIf(name, content, budget, fromTail = false) {
186
186
  // MEMORY.md is APPENDED to over time, so its newest (most relevant) notes
187
187
  // live at the TAIL — a head clip would drop exactly those. Keep the tail
188
188
  // for memory files, the head for AGENTS.md (whose top is the summary).
189
- if (!fromTail)
190
- return cleaned.slice(0, keep) + NL + NL + marker;
191
- let slice = cleaned.slice(cleaned.length - keep);
189
+ //
190
+ // The cut is in UTF-16 units, so it can land BETWEEN the halves of a
191
+ // surrogate PAIR (an emoji / astral CJK char): the head would then end with
192
+ // a lone HIGH half and the tail would START with a lone LOW half. Both reach
193
+ // the model through the system prompt as broken code points (the same rule
194
+ // as truncateToolResult / capReport / cutHookText). Step one unit back for
195
+ // the head, one unit forward for the tail, so the pair is dropped whole.
196
+ if (!fromTail) {
197
+ let end = keep;
198
+ const last = cleaned.charCodeAt(end - 1);
199
+ if (last >= 0xd800 && last <= 0xdbff)
200
+ end--;
201
+ return cleaned.slice(0, end) + NL + NL + marker;
202
+ }
203
+ let start = cleaned.length - keep;
204
+ const first = cleaned.charCodeAt(start);
205
+ if (first >= 0xdc00 && first <= 0xdfff)
206
+ start++;
207
+ let slice = cleaned.slice(start);
192
208
  // Cut at a NOTE boundary, not mid-note: start the kept tail at the next line
193
209
  // that begins a bullet or a heading, so a half-sentence note is not left.
194
210
  const m = slice.search(/\n(?=[-*] |#{1,6} )/);
@@ -245,7 +261,14 @@ const PROJECT_SKILL_DIRS = [
245
261
  'skills',
246
262
  ];
247
263
  function parseFrontmatter(raw) {
248
- const norm = raw.replace(CRLF_RE, NL);
264
+ // A leading BOM must be dropped BEFORE the `---` check: Windows editors and
265
+ // generators write one, and without this the frontmatter was not recognised
266
+ // at all (description / argument-hint / arguments silently lost).
267
+ // `skillBody` already strips it; the two must agree.
268
+ const bom = String.fromCharCode(0xfeff);
269
+ const norm = String(raw ?? '')
270
+ .replace(new RegExp('^' + bom), '')
271
+ .replace(CRLF_RE, NL);
249
272
  if (!norm.startsWith('---'))
250
273
  return {};
251
274
  const end = norm.indexOf(NL + '---', 3);
package/dist/diff.js CHANGED
@@ -2,22 +2,65 @@ import { theme } from './theme.js';
2
2
  export function unifiedDiff(a, b, { label = '' } = {}) {
3
3
  const la = a.split(String.fromCharCode(10));
4
4
  const lb = b.split(String.fromCharCode(10));
5
- const n = Math.max(la.length, lb.length);
6
5
  const out = [];
7
6
  if (label)
8
7
  out.push('... ' + label);
9
- for (let i = 0; i < n; i++) {
10
- const x = la[i];
11
- const y = lb[i];
12
- if (x === y)
8
+ // A LINE diff, not a positional compare: an insertion at the top of a
9
+ // file shifts every following line, and comparing line i with line i
10
+ // reported the WHOLE file as removed AND added (a 20-line file with one
11
+ // new first line produced 41 diff lines). The undo/`/diff` preview became
12
+ // unreadable and the model was told everything changed. A longest-common-
13
+ // subsequence walk emits only the lines that really differ.
14
+ for (const op of diffOps(la, lb)) {
15
+ if (op.kind === 'same')
13
16
  continue;
14
- if (x !== undefined)
15
- out.push('-' + x);
16
- if (y !== undefined)
17
- out.push('+' + y);
17
+ out.push((op.kind === 'del' ? '-' : '+') + op.line);
18
18
  }
19
19
  return out.join(String.fromCharCode(10));
20
20
  }
21
+ /**
22
+ * Longest-common-subsequence line diff (Hunt–Szymanski style table, O(n*m)
23
+ * memory). Files here are a handful of lines (a single edit preview), so the
24
+ * simple dynamic-programming table is fine and keeps the module dependency-
25
+ * free. A change pair is emitted as `-old` then `+new`, like a real diff.
26
+ */
27
+ function diffOps(a, b) {
28
+ const n = a.length;
29
+ const m = b.length;
30
+ // lcs[i][j] = length of the LCS of a[i..] and b[j..].
31
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
32
+ for (let i = n - 1; i >= 0; i--) {
33
+ for (let j = m - 1; j >= 0; j--) {
34
+ lcs[i][j] =
35
+ a[i] === b[j]
36
+ ? lcs[i + 1][j + 1] + 1
37
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
38
+ }
39
+ }
40
+ const ops = [];
41
+ let i = 0;
42
+ let j = 0;
43
+ while (i < n && j < m) {
44
+ if (a[i] === b[j]) {
45
+ ops.push({ kind: 'same', line: a[i] });
46
+ i++;
47
+ j++;
48
+ }
49
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
50
+ ops.push({ kind: 'del', line: a[i] });
51
+ i++;
52
+ }
53
+ else {
54
+ ops.push({ kind: 'add', line: b[j] });
55
+ j++;
56
+ }
57
+ }
58
+ while (i < n)
59
+ ops.push({ kind: 'del', line: a[i++] });
60
+ while (j < m)
61
+ ops.push({ kind: 'add', line: b[j++] });
62
+ return ops;
63
+ }
21
64
  export function colorDiff(diffText) {
22
65
  return diffText
23
66
  .split(String.fromCharCode(10))
@@ -226,6 +226,13 @@ export function createExtraTools(workdir, { undo, todos } = {}) {
226
226
  if (v === undefined || v === null) {
227
227
  throw new Error(`Missing required argument: ${name}. Add "${name}" to the tool call arguments and retry.`);
228
228
  }
229
+ // `path`/`pattern` are strings by contract; an OBJECT used to become
230
+ // "[object Object]" and reach the fs layer as a literal file name.
231
+ if (typeof v === 'object' || typeof v === 'function') {
232
+ throw new Error(`Invalid required argument ${name}: expected a string, got ` +
233
+ JSON.stringify(v) +
234
+ '. Re-send the call with a plain string value.');
235
+ }
229
236
  const s = String(v);
230
237
  if (s === '' || s === 'undefined' || s === 'null') {
231
238
  throw new Error(`Invalid required argument ${name}: ${JSON.stringify(v)}. Provide a non-empty value for "${name}" and retry.`);
package/dist/glm-chats.js CHANGED
@@ -157,9 +157,26 @@ export function parseGlmHistory(json) {
157
157
  : null;
158
158
  if (!arr)
159
159
  return [];
160
+ // The map is UNORDERED and `timestamp` restores the dialogue order. The
161
+ // value is a NUMBER or a numeric string in the builds seen so far, but an
162
+ // ISO stamp (`2026-10-23T10:00:00Z`) makes `Number(...)` NaN, and a NaN
163
+ // comparator keeps the INSERTION order — a scrambled dialogue. Fall back to
164
+ // Date.parse for a non-numeric string.
165
+ const stampOf = (m) => {
166
+ const raw = m.timestamp;
167
+ const n = Number(raw);
168
+ if (Number.isFinite(n) && n !== 0)
169
+ return n;
170
+ if (typeof raw === 'string') {
171
+ const t = Date.parse(raw);
172
+ if (Number.isFinite(t))
173
+ return t;
174
+ }
175
+ return 0;
176
+ };
160
177
  const sorted = arr
161
178
  .filter((x) => !!x && typeof x === 'object')
162
- .sort((a, b) => (Number(a.timestamp) || 0) - (Number(b.timestamp) || 0));
179
+ .sort((a, b) => stampOf(a) - stampOf(b));
163
180
  const out = [];
164
181
  for (const m of sorted) {
165
182
  // The role comes from `role` (confirmed by the live fixture). Only the
@@ -172,6 +172,10 @@ export function extractAnswer(body) {
172
172
  return sse;
173
173
  return extractFromJson(body);
174
174
  }
175
+ /** A usable token count: a FINITE NON-NEGATIVE number. */
176
+ function isUsageCount(v) {
177
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0;
178
+ }
175
179
  /**
176
180
  * The CONTEXT size (tokens) from `usage.prompt_tokens` - the prompt size for
177
181
  * the turn, the same idea as DeepSeek's accumulated_token_usage. Falls back to
@@ -186,9 +190,14 @@ export function extractTokenUsage(body) {
186
190
  if (!usage || typeof usage !== 'object')
187
191
  continue;
188
192
  const u = usage;
189
- if (typeof u.prompt_tokens === 'number' && Number.isFinite(u.prompt_tokens))
193
+ // A token count is a FINITE NON-NEGATIVE number. A negative payload (a
194
+ // nonsense reply, or a wrapped `int` in a proxy build) used to become
195
+ // `browser._lastTokenUsage` as-is: the context badge disappeared
196
+ // (tokenStatusLevel rejects it) while the auto-compact percentage went
197
+ // negative and never fired. Treat it as "no usage", like a non-number.
198
+ if (isUsageCount(u.prompt_tokens))
190
199
  lastPrompt = u.prompt_tokens;
191
- if (typeof u.total_tokens === 'number' && Number.isFinite(u.total_tokens))
200
+ if (isUsageCount(u.total_tokens))
192
201
  lastTotal = u.total_tokens;
193
202
  }
194
203
  if (lastPrompt !== null)
@@ -205,9 +214,9 @@ export function extractTokenUsage(body) {
205
214
  : null;
206
215
  const usage = ((envelope ? envelope.usage : undefined) ?? obj?.usage);
207
216
  if (usage) {
208
- if (typeof usage.prompt_tokens === 'number')
217
+ if (isUsageCount(usage.prompt_tokens))
209
218
  return usage.prompt_tokens;
210
- if (typeof usage.total_tokens === 'number')
219
+ if (isUsageCount(usage.total_tokens))
211
220
  return usage.total_tokens;
212
221
  }
213
222
  }
@@ -279,7 +288,16 @@ const ERROR_PAYLOAD_RE = /"type"\s*:\s*"error"|"error"\s*:|\{\s*"code"\s*:/i;
279
288
  * message instead of retrying for the whole budget.
280
289
  */
281
290
  export function isQuotaExhaustedText(body) {
282
- return /insufficient_quota|quota (?:exceeded|exhausted)|exceeded your current quota/i.test(String(body || ''));
291
+ const b = String(body || '');
292
+ if (!b)
293
+ return false;
294
+ // Same guard as every other z.ai classifier: a normal ANSWER may legitimately
295
+ // discuss `insufficient_quota` / "exceeded your current quota" (the operator
296
+ // asked how to handle that API error). Without this the answer was taken for
297
+ // a refusal and a good turn was abandoned with `glm.quota_exhausted`.
298
+ if (!ERROR_PAYLOAD_RE.test(b) && hasAnswerContent(b))
299
+ return false;
300
+ return /insufficient_quota|quota (?:exceeded|exhausted)|exceeded your current quota/i.test(b);
283
301
  }
284
302
  export function isRateLimitText(body) {
285
303
  const b = String(body || '');
package/dist/goal.js CHANGED
@@ -96,7 +96,12 @@ export function parseGoalVerdict(text) {
96
96
  // separator is checked on the character FOLLOWING the matched value (a
97
97
  // lookahead INSIDE the value pattern was useless: the value alternatives
98
98
  // always end on a word boundary of their own).
99
- const label = /VERDICT\s*[*_`]*\s*:\s*[*_`]*\s*(IMPOSSIBLE|NOT[_ -]?MET|MET)/gi;
99
+ // A right boundary is REQUIRED: without it `VERDICT: METS` /
100
+ // `METADATA` parsed as MET, and MET CLEARS the goal — an unfinished task
101
+ // looked done. `\b` alone is not enough: in `MET-ish` the hyphen is a word
102
+ // boundary, so a hyphen/underscore directly after the value is rejected
103
+ // explicitly (a hyphen is only legal INSIDE `NOT-MET`).
104
+ const label = /VERDICT\s*[*_`]*\s*:\s*[*_`]*\s*(IMPOSSIBLE|NOT[_ -]?MET|MET)(?![\w-])/gi;
100
105
  let m;
101
106
  let status = null;
102
107
  while ((m = label.exec(t)) !== null) {
@@ -96,9 +96,9 @@ export const deepseekMessages = {
96
96
  ru: '✖ {provider} перестал отвечать, лимит повторов исчерпан. Модель не дала ответа — проверь чат вручную.',
97
97
  en: '✖ {provider} stopped responding, retry limit exhausted. The model gave no answer — check the chat manually.',
98
98
  },
99
- 'ds.tool_retry': {
100
- ru: '⏳ Агент не распознал ответ модели — прошу продолжить ({attempt}/{max})...',
101
- en: '⏳ The agent did not recognize the model answer — asking it to continue ({attempt}/{max})...',
99
+ 'ds.text_no_tool': {
100
+ ru: '⏳ Модель ответила текстом без вызова инструмента ({attempt}/{max}) — прошу вызвать инструмент...',
101
+ en: '⏳ The model answered with plain text and no tool call ({attempt}/{max}) — asking for a tool call...',
102
102
  },
103
103
  'ds.not_launched': {
104
104
  ru: '✖ Браузер не запущен.',
@@ -183,4 +183,8 @@ export const selfReviewMessages = {
183
183
  ru: 'Использование: /resume-id <chat id>',
184
184
  en: 'Usage: /resume-id <chat id>',
185
185
  },
186
+ 'sessions.provider_mismatch': {
187
+ ru: 'Сессия принадлежит провайдеру {owner}, а активен {active}: чат не откроется. Переключись командой /provider {owner} и повтори.',
188
+ en: 'This session belongs to {owner}, but {active} is active: the chat will not open. Switch with /provider {owner} and retry.',
189
+ },
186
190
  };
package/dist/i18n.js CHANGED
@@ -37,7 +37,7 @@ export function normalizeLocale(v) {
37
37
  return DEFAULT_LOCALE;
38
38
  }
39
39
  // String catalog. Key -> { ru, en }. If a key is missing we return the key itself.
40
- // Split into section files (C3 step 1); merged here so callers keep importing
40
+ // Split into section files; merged here so callers keep importing
41
41
  // CATALOG from a single place.
42
42
  export const CATALOG = {
43
43
  ...commonMessages,
@@ -55,7 +55,13 @@ export const CATALOG = {
55
55
  export function translate(locale) {
56
56
  const loc = isLocale(locale) ? locale : DEFAULT_LOCALE;
57
57
  return (key, params) => {
58
- const entry = CATALOG[key];
58
+ // A plain-object lookup ALSO resolves Object.prototype members, so a key
59
+ // like 'toString' returned the inherited FUNCTION: the caller got
60
+ // `undefined` (no params) or a TypeError (`.replace is not a function`).
61
+ // Only an OWN entry is a translation.
62
+ const entry = Object.prototype.hasOwnProperty.call(CATALOG, key)
63
+ ? CATALOG[key]
64
+ : undefined;
59
65
  const template = entry ? entry[loc] || entry[DEFAULT_LOCALE] : key;
60
66
  if (!params)
61
67
  return template;