zames_pro 2.75.1 → 2.75.2

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.75.2] - 2026-10-15
11
+
12
+ ### Changed
13
+
14
+ - batch read-only tool calls and skip redundant checkpoints
15
+
10
16
  ## [2.75.1] - 2026-10-15
11
17
 
12
18
  ### Added
@@ -180,6 +180,15 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
180
180
  // block after the tool loop).
181
181
  let malformedRetries = 0;
182
182
  const MAX_MALFORMED_RETRIES = 3;
183
+ // Tool-result caps for the message fed back to the model. A single result
184
+ // gets a generous cap; in a BATCH each item is capped and the ASSEMBLED
185
+ // message is capped again, so a big batch cannot exceed what the chat input
186
+ // accepts (a failed paste loses the whole turn). The caps are explicit:
187
+ // truncateToolResult() appends `[...truncated N chars]` so the model knows
188
+ // it is looking at a partial output.
189
+ const TOOL_RESULT_LIMIT = 24_000;
190
+ const BATCH_ITEM_LIMIT = 16_000;
191
+ const BATCH_TOTAL_LIMIT = 120_000;
183
192
  let stallRetries = 0;
184
193
  const MAX_STALL_RETRIES = 5;
185
194
  // SINGLE shared budget for "the answer is not a recognized tool call".
@@ -752,6 +761,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
752
761
  // and no call matches an `ask` permission rule (a concurrent operator
753
762
  // prompt would race). Anything else keeps the strict sequential loop, so
754
763
  // mutating side effects never reorder.
764
+ //
765
+ // MUTATING tools (Write/Edit/MultiEdit) are deliberately NOT here even
766
+ // though writes to different files are logically independent: (a)
767
+ // UndoStore.backup() does a read-modify-write of ONE shared index file, so
768
+ // two concurrent backups race and lose a record; (b) PostToolUse hooks and
769
+ // diagnostics must run in a defined order. A batch of mutating calls still
770
+ // costs only ONE round-trip (the sequential loop below runs them all for
771
+ // the same answer), so nothing is lost by keeping them sequential.
755
772
  const PARALLEL_SAFE = new Set(['Read', 'Glob', 'Grep', 'LS', 'WebFetch']);
756
773
  const readOnlyBatch = callsToRun.length > 1 &&
757
774
  !(hookConfig.PreToolUse && hookConfig.PreToolUse.length) &&
@@ -1064,15 +1081,23 @@ ${diag}`;
1064
1081
  if (results.length === 1) {
1065
1082
  const r = results[0];
1066
1083
  const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
1067
- message = `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, 12_000)}`;
1084
+ message = `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, TOOL_RESULT_LIMIT)}`;
1068
1085
  }
1069
1086
  else {
1070
1087
  message = results
1071
1088
  .map((r) => {
1072
1089
  const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
1073
- return `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, 8000)}`;
1090
+ return `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, BATCH_ITEM_LIMIT)}`;
1074
1091
  })
1075
1092
  .join('\n\n');
1093
+ // A large batch (e.g. 8 Reads) could still add up past what the chat
1094
+ // input accepts; a too-long message fails to paste (ds.input_partial) and
1095
+ // the whole turn is lost. Cap the ASSEMBLED message as a safety net,
1096
+ // keeping the per-item truncation markers (the model still sees WHAT was
1097
+ // dropped, just a bit more of it).
1098
+ if (message.length > BATCH_TOTAL_LIMIT) {
1099
+ message = truncateToolResult(message, BATCH_TOTAL_LIMIT);
1100
+ }
1076
1101
  }
1077
1102
  // Append the argument-error nudge (set inside the batch loop) now that
1078
1103
  // `message` has been rebuilt from `results`. Cleared so it fires once per
package/dist/browser.js CHANGED
@@ -1895,8 +1895,11 @@ export class DeepSeekBrowser {
1895
1895
  }
1896
1896
  // Reasoning-mode pause during the THINK phase: click Continue so the
1897
1897
  // generation resumes (the button appears before any RESPONSE text, so
1898
- // the start-wait would otherwise spin until the deadline).
1899
- if (this.autoContinue && (await this._clickContinueIfVisible())) {
1898
+ // the start-wait would otherwise spin until the deadline). Probing
1899
+ // PROACTIVELY is meaningful only with the reasoning toggle ON.
1900
+ if (this.autoContinue &&
1901
+ this.deepThinking &&
1902
+ (await this._clickContinueIfVisible())) {
1900
1903
  this._askDebug('CLICKED Continue (start-loop)');
1901
1904
  }
1902
1905
  const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
@@ -1935,8 +1938,12 @@ export class DeepSeekBrowser {
1935
1938
  const notGenerating = !(await this._isGenerating());
1936
1939
  const differs = !!cur && normText(cur) !== normText(beforeText);
1937
1940
  // Do NOT accept a "settled" answer while Continue is on screen — that
1938
- // is a paused generation, not a finished one.
1939
- const paused = this.autoContinue && (await this._continueButtonVisible());
1941
+ // is a paused generation, not a finished one. The paused check only
1942
+ // applies with the reasoning toggle ON (the THINK-phase pause); without
1943
+ // it a stable answer is a genuinely finished one.
1944
+ const paused = this.autoContinue &&
1945
+ this.deepThinking &&
1946
+ (await this._continueButtonVisible());
1940
1947
  if (differs && cur === lastStartCur && notGenerating && !paused) {
1941
1948
  settledTicks++;
1942
1949
  if (settledTicks >= 2) {
@@ -2095,8 +2102,15 @@ export class DeepSeekBrowser {
2095
2102
  }
2096
2103
  // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
2097
2104
  // Continue button; the model does not resume by itself. Click it so the
2098
- // answer keeps flowing instead of waiting for the operator.
2099
- if (this.autoContinue && (await this._clickContinueIfVisible())) {
2105
+ // answer keeps flowing instead of waiting for the operator. Only probe
2106
+ // PROACTIVELY when the reasoning (Deep thinking) toggle is ON: the
2107
+ // THINK-phase pause does not occur without it, so an idle probe on every
2108
+ // tick is a wasted evaluate. The truncated / finished-without-answer
2109
+ // branches above stay unconditional — those are server faults, not a
2110
+ // reasoning pause, and occur with the toggle off too.
2111
+ if (this.autoContinue &&
2112
+ this.deepThinking &&
2113
+ (await this._clickContinueIfVisible())) {
2100
2114
  this._askDebug('CLICKED Continue (finish-loop)');
2101
2115
  continue;
2102
2116
  }
@@ -2156,8 +2170,11 @@ export class DeepSeekBrowser {
2156
2170
  // A PAUSED generation also looks "stable" (the text stops changing
2157
2171
  // while Continue is on screen), so the old check returned the
2158
2172
  // partial answer here — the "agent stopped with a Continue button"
2159
- // symptom. Click Continue and keep waiting instead.
2160
- if (this.autoContinue && (await this._continueButtonVisible())) {
2173
+ // symptom. Click Continue and keep waiting instead. Only relevant
2174
+ // with the reasoning toggle ON (the THINK-phase pause).
2175
+ if (this.autoContinue &&
2176
+ this.deepThinking &&
2177
+ (await this._continueButtonVisible())) {
2161
2178
  if (await this._clickContinueIfVisible()) {
2162
2179
  this._askDebug('CLICKED Continue (before early return)');
2163
2180
  }
@@ -2205,7 +2222,8 @@ export class DeepSeekBrowser {
2205
2222
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
2206
2223
  // If Continue is STILL on screen, the turn is paused, not finished:
2207
2224
  // click once more and report what we have rather than silently stopping.
2208
- if (this.autoContinue)
2225
+ // Only with the reasoning toggle ON (the THINK-phase pause).
2226
+ if (this.autoContinue && this.deepThinking)
2209
2227
  await this._clickContinueIfVisible();
2210
2228
  this._askDebug('RETURN last len=' + last.length);
2211
2229
  return last;
@@ -69,6 +69,19 @@ function runTar(args, cwd, timeout = 60_000) {
69
69
  });
70
70
  });
71
71
  }
72
+ // Git fingerprint of a working tree: HEAD plus the porcelain status. Two
73
+ // snapshots with the same fingerprint hold the same TRACKED state, so a
74
+ // second checkpoint would be a duplicate. Returns null for a non-git dir (or
75
+ // when git is unavailable) — then the caller always snapshots (no debounce).
76
+ function workdirFingerprint(workdir) {
77
+ return new Promise((resolve) => {
78
+ execFile('git', ['-C', workdir, 'status', '--porcelain=v1', '--branch'], { timeout: 10_000, maxBuffer: 1024 * 1024, windowsHide: true }, (err, stdout) => {
79
+ if (err)
80
+ return resolve(null);
81
+ resolve(String(stdout || '').trim() || 'clean');
82
+ });
83
+ });
84
+ }
72
85
  async function copyTree(from, to) {
73
86
  await fs.mkdir(to, { recursive: true });
74
87
  const entries = await fs.readdir(from, { withFileTypes: true });
@@ -116,10 +129,34 @@ export class CheckpointStore {
116
129
  * unreadable dir or a timeout yields null, never a thrown error — a failed
117
130
  * checkpoint must not abort the task it was meant to protect.
118
131
  */
119
- async create(workdir, label) {
132
+ async create(workdir, label,
133
+ /** When false, always create a new archive (skip the fingerprint dedupe). */
134
+ allowDedupe = true) {
120
135
  if (!this.enabled)
121
136
  return null;
122
137
  await this._ensure();
138
+ // Debounce: a checkpoint is taken before EVERY task, and consecutive
139
+ // tasks in one queue often start with an IDENTICAL tracked state (the
140
+ // previous task made no commit and no tracked edit that survived).
141
+ // Re-taring the whole tree then only burns time. If the last record for
142
+ // this workdir carries the SAME git fingerprint, reuse it instead of
143
+ // snapshotting again. Non-git dirs get a null fingerprint and are always
144
+ // snapshotted (we cannot cheaply prove the tree is unchanged).
145
+ const fingerprint = allowDedupe ? await workdirFingerprint(workdir) : null;
146
+ if (fingerprint) {
147
+ const history = await this._readIndex();
148
+ const last = [...history]
149
+ .reverse()
150
+ .find((r) => r.workdir === workdir && r.fingerprint);
151
+ if (last &&
152
+ last.fingerprint === fingerprint &&
153
+ (await fs
154
+ .access(last.file)
155
+ .then(() => true)
156
+ .catch(() => false))) {
157
+ return last;
158
+ }
159
+ }
123
160
  const stamp = Date.now();
124
161
  // Two checkpoints in the SAME millisecond would share `${stamp}.tgz` and
125
162
  // the second would overwrite the first (losing a snapshot). Add a random
@@ -134,6 +171,8 @@ export class CheckpointStore {
134
171
  const record = { stamp, file, workdir };
135
172
  if (label)
136
173
  record.label = label;
174
+ if (fingerprint)
175
+ record.fingerprint = fingerprint;
137
176
  const history = await this._readIndex();
138
177
  history.push(record);
139
178
  const kept = rotateRecords(history, this.maxBackups);
@@ -171,7 +210,7 @@ export class CheckpointStore {
171
210
  if (!exists)
172
211
  return { ok: false, reason: 'archive_missing' };
173
212
  // Backup the CURRENT tree before touching it.
174
- const backup = await this.create(record.workdir, 'pre-rewind');
213
+ const backup = await this.create(record.workdir, 'pre-rewind', false);
175
214
  // Extract to a temp dir FIRST; only a successful extract may touch workdir
176
215
  // (otherwise a bad archive would wipe the tree).
177
216
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'zames-rewind-'));
@@ -234,6 +234,33 @@ Prefer the most specific tool; a wrong choice wastes a turn:
234
234
  - Reading a file whose path you already know -> Read, not Bash cat.
235
235
  - A broad search/exploration that would flood this chat -> the Task tool
236
236
  (a subagent in its own chat) if it is available, then work from its report.
237
+
238
+ ## Batch independent calls (IMPORTANT: saves round-trips)
239
+
240
+ Every separate turn costs a full send + throttle pause, so DO NOT dribble
241
+ calls out one at a time when you already know several are needed. When the
242
+ next step requires reading, searching or writing SEVERAL known targets, emit
243
+ ONE JSON array with all of them in a single answer:
244
+
245
+ [{"tool": "Read", "args": {"path": "a.ts"}},
246
+ {"tool": "Read", "args": {"path": "b.ts"}},
247
+ {"tool": "Grep", "args": {"pattern": "foo", "include": "*.ts"}}]
248
+
249
+ Rules for a batch:
250
+ - BATCH only INDEPENDENT calls: no call may depend on another call's result
251
+ (do not batch a Read and an Edit of the same file, and never batch two
252
+ writes to the SAME path).
253
+ - Read-only calls (Read, Glob, Grep, LS, WebFetch) run CONCURRENTLY, so a
254
+ batch of them is several times faster than the same calls one by one.
255
+ - Writing to SEVERAL DIFFERENT files can be batched too (e.g. three Write
256
+ calls to three different new paths, or Edits to different files). Mutating
257
+ calls in a batch run in order, so a batch still saves the extra round-trips
258
+ even though the writes themselves are serialized.
259
+ - If the next step depends on what a call RETURNS, that call must be alone
260
+ (or be the last one in the batch).
261
+ - Typical wins: read all the files a task mentions at once; grep for all the
262
+ symbols you need at once; after planning a multi-file change, write all the
263
+ files you can write independently at once.
237
264
  ${subagentSection}
238
265
  ## MCP tools
239
266
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.1",
3
+ "version": "2.75.2",
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": {