zames_pro 2.82.139 → 2.82.140

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,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.82.140] - 2026-10-25
11
+
12
+ ### Fixed
13
+
14
+ - **scheduler:** stop the ticker on exit; correct the abort help line
15
+ - **config:** say when a project config overrides the home one
16
+
10
17
  ## [2.82.139] - 2026-10-25
11
18
 
12
19
  ### Changed
@@ -15,8 +15,22 @@ import { configGetValue, configValueHint } from './config-commands.js';
15
15
  * blindly from the main loop and from the live mid-run interception.
16
16
  */
17
17
  export async function handleConfigCommand(input, deps) {
18
- const { t, print, printErr, getValue, setValue, resetField, showList, openMenu, currentLocale, normalizeLocale, localeDisplayName, homeConfigPath, projectConfigPath, } = deps;
18
+ const { t, print, printErr, getValue, setValue, resetField, showList, openMenu, currentLocale, normalizeLocale, localeDisplayName, homeConfigPath, projectConfigPath, shadowedKeys, } = deps;
19
19
  const schema = deps.schema;
20
+ // A HOME write is not always the value the next launch reads: the project
21
+ // `.zamesrc.json` is merged last, so a key present in both wins from the
22
+ // project file. Without this line the operator saw "saved" and then the OLD
23
+ // value after a restart (the stale `ui.locale: en` bug).
24
+ const warnShadowed = (key) => {
25
+ if (!shadowedKeys)
26
+ return;
27
+ if (!shadowedKeys().includes(key))
28
+ return;
29
+ printErr(theme.warn(t('cfg.shadowed', {
30
+ file: projectConfigPath,
31
+ keys: key,
32
+ })));
33
+ };
20
34
  // Accept BOTH spellings: the main loop passes the whole typed line
21
35
  // (`/config get ui.locale`), while tests and other callers pass the text
22
36
  // after the command (`get ui.locale`). Reading parts[1] blindly made the
@@ -60,6 +74,7 @@ export async function handleConfigCommand(input, deps) {
60
74
  return;
61
75
  }
62
76
  print(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
77
+ warnShadowed('ui.locale');
63
78
  return;
64
79
  }
65
80
  if (sub === 'get') {
@@ -105,6 +120,7 @@ export async function handleConfigCommand(input, deps) {
105
120
  value: configGetValue(key, getValue(key), t),
106
121
  file: homeConfigPath,
107
122
  })));
123
+ warnShadowed(key);
108
124
  return;
109
125
  }
110
126
  if (sub === 'reset' || sub === 'unset') {
package/dist/config.js CHANGED
@@ -165,19 +165,72 @@ export const DEFAULTS = {
165
165
  };
166
166
  export function loadConfig() {
167
167
  const merged = JSON.parse(JSON.stringify(DEFAULTS));
168
- for (const file of [HOME_CONFIG, projectConfigPath()]) {
169
- try {
170
- const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
171
- deepMerge(merged, data);
172
- }
173
- catch (e) {
174
- const err = e;
175
- if (err.code !== 'ENOENT') {
176
- console.error(`config: failed to read ${file}: ${err.message}`);
168
+ // Keys the PROJECT file overrides in the HOME file. Kept so the editor can
169
+ // say "your change was saved but the project file still wins" — a live
170
+ // symptom was `/config lang ru` answering "saved", then English after a
171
+ // restart, because a stale project `.zamesrc.json` pinned `ui.locale: en`.
172
+ shadowedKeys = [];
173
+ const homeData = readConfigObject(HOME_CONFIG);
174
+ const projectData = readConfigObject(projectConfigPath());
175
+ if (projectData) {
176
+ shadowedKeys = overriddenKeys(homeData, projectData);
177
+ }
178
+ for (const data of [homeData, projectData]) {
179
+ if (!data)
180
+ continue;
181
+ deepMerge(merged, data);
182
+ }
183
+ return merged;
184
+ }
185
+ /** Keys the lower-priority file also sets, from the last loadConfig(). */
186
+ let shadowedKeys = [];
187
+ export function shadowedConfigKeys() {
188
+ return shadowedKeys.slice();
189
+ }
190
+ /**
191
+ * Every leaf path ("ui.locale", "browser.auth.username") present in BOTH
192
+ * objects — i.e. a value the PROJECT config overrides over the HOME one.
193
+ * PURE so the precedence puzzle can be tested without touching the disk.
194
+ */
195
+ export function overriddenKeys(lower, higher) {
196
+ if (!lower || !higher)
197
+ return [];
198
+ const out = [];
199
+ const walk = (a, b, prefix) => {
200
+ for (const key of Object.keys(b)) {
201
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
202
+ continue;
203
+ }
204
+ const path2 = prefix ? prefix + '.' + key : key;
205
+ const av = a[key];
206
+ const bv = b[key];
207
+ if (isPlainObject(av) && isPlainObject(bv)) {
208
+ walk(av, bv, path2);
209
+ }
210
+ else if (av !== undefined && !isPlainObject(bv)) {
211
+ out.push(path2);
177
212
  }
178
213
  }
214
+ };
215
+ walk(lower, higher, '');
216
+ return out.sort();
217
+ }
218
+ function isPlainObject(v) {
219
+ return !!v && typeof v === 'object' && !Array.isArray(v);
220
+ }
221
+ /** Parse one config file; null when it is missing or unreadable. */
222
+ function readConfigObject(file) {
223
+ try {
224
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
225
+ return isPlainObject(data) ? data : null;
226
+ }
227
+ catch (e) {
228
+ const err = e;
229
+ if (err.code !== 'ENOENT') {
230
+ console.error(`config: failed to read ${file}: ${err.message}`);
231
+ }
232
+ return null;
179
233
  }
180
- return merged;
181
234
  }
182
235
  // The agent-loop watchdog deadline per provider. The loop default (240s) is
183
236
  // tuned for DeepSeek; a Qwen GUEST turn regularly takes 7-8 minutes (cold
@@ -251,6 +251,13 @@ export const configMessages = {
251
251
  ru: 'Язык переключён на {v}.',
252
252
  en: 'Language switched to {v}.',
253
253
  },
254
+ // The project `.zamesrc.json` is merged AFTER the home config, so a key it
255
+ // also sets silently wins — the operator sees "saved", then the old value
256
+ // after a restart (a stale `ui.locale: en` looked like a lost setting).
257
+ 'cfg.shadowed': {
258
+ ru: '⚠ Проектный конфиг {file} переопределяет: {keys} — после перезапуска будет его значение. Уберите эти ключи из проектного файла.',
259
+ en: '⚠ The project config {file} overrides: {keys} — after a restart its value wins. Remove these keys from the project file.',
260
+ },
254
261
  'cfg.lang_usage': {
255
262
  ru: 'Использование: /config lang <ru|en>',
256
263
  en: 'Usage: /config lang <ru|en>',
package/dist/i18n/help.js CHANGED
@@ -94,8 +94,8 @@ export const helpMessages = {
94
94
  en: 'Ctrl+Shift+V image paste an image/file (saved to tmp)',
95
95
  },
96
96
  'help.key.esc': {
97
- ru: 'Esc, Ctrl+C прервать текущую генерацию',
98
- en: 'Esc, Ctrl+C abort current generation',
97
+ ru: 'Esc, Ctrl+C Esc — прервать генерацию; Ctrl+C — прервать инструмент, второй раз — весь прогон',
98
+ en: 'Esc, Ctrl+C Esc — abort generation; Ctrl+C — abort the tool, twice — the whole run',
99
99
  },
100
100
  'help.key.scroll': {
101
101
  ru: 'PgUp / PgDn вверх/вниз по диалогу (Home/End — в начало/конец) [full-screen]',
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { LineEditor } from './input.js';
20
20
  import { TuiEditor } from './tui/TuiEditor.js';
21
21
  import { promptOnce } from './prompt.js';
22
22
  import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
23
- import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, } from './config.js';
23
+ import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, shadowedConfigKeys, } from './config.js';
24
24
  import { runConfigMenu } from './config-menu.js';
25
25
  import { runConfigMenuInk } from './tui/run-config-menu.js';
26
26
  import { formatConfigList } from './config-commands.js';
@@ -126,6 +126,17 @@ let currentLocale = isLocale(config.ui?.locale)
126
126
  // Translation: reads currentLocale at call time, so a language change takes
127
127
  // effect immediately, without a restart (for lines printed afterwards).
128
128
  const t = (key, params) => translate(currentLocale)(key, params);
129
+ // A project `.zamesrc.json` wins over the HOME config, so a key it ALSO sets
130
+ // silently defeats `/config set` — the value looks saved and comes back stale
131
+ // after a restart (the reported "language resets to English" case). Say it at
132
+ // startup, before a task, instead of letting the operator hunt for it.
133
+ const shadowedAtStartup = shadowedConfigKeys();
134
+ if (shadowedAtStartup.length) {
135
+ console.error(theme.warn(t('cfg.shadowed', {
136
+ file: CONFIG_PATHS.PROJECT_CONFIG,
137
+ keys: shadowedAtStartup.join(', '),
138
+ })));
139
+ }
129
140
  // Headless is the default. --headed forces a visible window (useful for the
130
141
  // first sign-in or debugging the DOM). --headless keeps it explicit.
131
142
  // DeepSeek/Qwen run headless by default; GLM CANNOT: z.ai answers a headless
@@ -1349,10 +1360,23 @@ async function main() {
1349
1360
  // never bypassing the send throttle). Started lazily on the first job.
1350
1361
  const scheduler = new Scheduler();
1351
1362
  let scheduleTimer = null;
1363
+ // Ctrl+C on an idle agent EXITS the process. Without this latch a /loop job
1364
+ // whose task was interrupted (and every later fire) would be dropped into
1365
+ // the queue while nobody waits for it, and each tick printed "fired" into a
1366
+ // dying terminal. A loop is meant to outlive a single task, not the agent.
1367
+ let shuttingDown = false;
1368
+ const stopScheduleTicker = () => {
1369
+ if (scheduleTimer) {
1370
+ clearInterval(scheduleTimer);
1371
+ scheduleTimer = null;
1372
+ }
1373
+ };
1352
1374
  const startScheduleTicker = () => {
1353
1375
  if (scheduleTimer)
1354
1376
  return;
1355
1377
  scheduleTimer = setInterval(() => {
1378
+ if (shuttingDown)
1379
+ return;
1356
1380
  const due = scheduler.due(Date.now());
1357
1381
  for (const job of due) {
1358
1382
  // Do not pile MANY copies of the same job while the agent is busy: if
@@ -1403,6 +1427,8 @@ async function main() {
1403
1427
  let reviewMode = null;
1404
1428
  process.on('SIGINT', async () => {
1405
1429
  running = false;
1430
+ shuttingDown = true;
1431
+ stopScheduleTicker();
1406
1432
  // Kill any background processes so no dev-server outlives the agent.
1407
1433
  killAllBackground();
1408
1434
  await browser.close().catch(() => { });
@@ -1827,8 +1853,12 @@ async function main() {
1827
1853
  lastCtrlCAt = now;
1828
1854
  }
1829
1855
  else {
1830
- // Not busy — exit. We wake takeInput() so the loop finishes.
1856
+ // Not busy — exit. We wake takeInput() so the loop finishes. The
1857
+ // scheduler must stop too: a live /loop ticker keeps firing into the
1858
+ // queue while the process is shutting down.
1831
1859
  running = false;
1860
+ shuttingDown = true;
1861
+ stopScheduleTicker();
1832
1862
  if (waiter) {
1833
1863
  const r = waiter;
1834
1864
  waiter = null;
@@ -2160,6 +2190,7 @@ async function main() {
2160
2190
  schema: CONFIG_SCHEMA,
2161
2191
  homeConfigPath: CONFIG_PATHS.HOME_CONFIG,
2162
2192
  projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
2193
+ shadowedKeys: shadowedConfigKeys,
2163
2194
  });
2164
2195
  }
2165
2196
  // Open a chat by its ChatInfo and print the restored dialogue. Shared by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.139",
3
+ "version": "2.82.140",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {