zames_pro 2.82.55 → 2.82.56

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,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.82.56] - 2026-10-23
11
+
12
+ ### Fixed
13
+
14
+ - **tool-summary:** mark an MCP failure as an error, not a success
15
+ - **tools:** answer Read failures in the tool's own words
16
+ - **paste:** never swap the bodies of two same-size pastes
17
+
10
18
  ## [2.82.55] - 2026-10-23
11
19
 
12
20
  ### Fixed
@@ -64,28 +64,77 @@ export function pasteReplacement(raw) {
64
64
  return null;
65
65
  return { marker: formatPasteMarker(lines), text };
66
66
  }
67
- // Replace "[Pasted lines#N]" markers with the original pasted text. The marker
68
- // carries ONLY the line count, so two pastes of the same length share it: a
69
- // plain indexOf-per-paste substituted the FIRST paste's text into whichever
70
- // marker survived (the operator deleted the first one) — the model then got
71
- // the WRONG paste under a plausible marker. Replacing ONE occurrence per paste
72
- // and tying each marker to the pasted text it was CREATED with fixes it for
73
- // every order: an unmatched paste leaves the marker as text instead of
74
- // swapping bodies.
75
- export function expandPastes(pastes, text) {
67
+ // Replace "[Pasted lines#N]" markers with the original pasted text.
68
+ //
69
+ // The marker carries ONLY the line count, so two pastes of the same length are
70
+ // indistinguishable in the buffer. The old "last occurrence for the last paste"
71
+ // walk handed the SURVIVING marker the wrong body whenever the operator deleted
72
+ // the other one. The caller can also name the paste it deleted
73
+ // (`deletedIndex`), which removes the ambiguity completely; without it, an
74
+ // ambiguous case (two same-size pastes, exactly one marker left) cannot be
75
+ // resolved from the text, so the marker is left AS IS — sending the literal
76
+ // "[Pasted lines#3]" is visible to the operator, while the wrong body under a
77
+ // plausible marker silently sends the model someone else's text.
78
+ export function expandPastes(pastes, text, deletedIndex) {
76
79
  if (!pastes.length)
77
80
  return text;
78
- // Expand in REVERSE order of appearance: each paste consumes its OWN marker
79
- // occurrence (the last one for the last paste), so a marker the operator
80
- // deleted cannot shift another paste's text into the wrong slot. With a
81
- // single occurrence left, the LAST paste owns it — the operator deleted the
82
- // EARLIER marker, so the later paste's body is the right fill.
83
- let out = String(text);
84
- for (let i = pastes.length - 1; i >= 0; i--) {
85
- const p = pastes[i];
86
- const idx = out.lastIndexOf(p.marker);
81
+ const src = String(text);
82
+ // Without a deletion hint, expansion is only unambiguous when the number of
83
+ // surviving markers matches the number of pastes (nothing was deleted) or
84
+ // the markers differ (different line counts).
85
+ const occurrences = new Map();
86
+ for (const p of pastes) {
87
+ occurrences.set(p.marker, (occurrences.get(p.marker) || 0) + 1);
88
+ }
89
+ let ambiguous = false;
90
+ for (const [marker, count] of occurrences) {
91
+ let seen = 0;
92
+ let from = 0;
93
+ while (true) {
94
+ const idx = src.indexOf(marker, from);
95
+ if (idx === -1)
96
+ break;
97
+ seen++;
98
+ from = idx + marker.length;
99
+ }
100
+ if (seen === count)
101
+ continue;
102
+ // Fewer markers than pastes means one was deleted; more than one survivor
103
+ // of the same text cannot be attributed.
104
+ if (seen > 1 || count > 1)
105
+ ambiguous = true;
106
+ }
107
+ if (deletedIndex !== undefined)
108
+ ambiguous = false;
109
+ // With a KNOWN deletion index the mapping is exact: the remaining pastes in
110
+ // order own the remaining markers in order.
111
+ const pool = deletedIndex !== undefined &&
112
+ deletedIndex >= 0 &&
113
+ deletedIndex < pastes.length
114
+ ? pastes.filter((_p, i) => i !== deletedIndex)
115
+ : pastes;
116
+ let out = src;
117
+ let scanFrom = 0;
118
+ const positions = [];
119
+ for (const p of pool) {
120
+ const idx = out.indexOf(p.marker, scanFrom);
87
121
  if (idx === -1)
88
122
  continue;
123
+ positions.push({ p, idx });
124
+ scanFrom = idx + p.marker.length;
125
+ }
126
+ const survivors = pool.length;
127
+ const total = pastes.length;
128
+ const lost = total - pool.length;
129
+ const found = positions.length;
130
+ // Every marker left in the text must be accounted for, and every unmatched
131
+ // paste must have actually lost its marker. Anything else is ambiguous.
132
+ if (!ambiguous && (found !== survivors || lost < 0))
133
+ ambiguous = true;
134
+ if (ambiguous)
135
+ return src;
136
+ for (let i = positions.length - 1; i >= 0; i--) {
137
+ const { p, idx } = positions[i];
89
138
  out = out.slice(0, idx) + p.text + out.slice(idx + p.marker.length);
90
139
  }
91
140
  return out;
@@ -97,6 +97,11 @@ export function looksLikeToolError(result) {
97
97
  head.startsWith('blocked by') ||
98
98
  head.startsWith('denied by') ||
99
99
  head.startsWith('unknown tool') ||
100
+ // An MCP failure is returned AS the result (see renderMcpResult):
101
+ // `MCP error: …` for an isError answer, `MCP error (server/tool): …`
102
+ // when the call throws. Neither was recognised, so a failed MCP call was
103
+ // drawn with a green check.
104
+ head.startsWith('mcp error') ||
100
105
  head.startsWith('subagents are not available') ||
101
106
  head.startsWith('subagent failed') ||
102
107
  head.startsWith('subagent error') ||
package/dist/tools.js CHANGED
@@ -107,7 +107,30 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
107
107
  // Binary guard (N35): reading an image/archive as utf-8 produced
108
108
  // mojibake the model tried to reason about. Detect a binary buffer
109
109
  // BEFORE decoding and answer with useful metadata instead.
110
- const buf = await fs.readFile(file);
110
+ // A read FAILURE (a missing file, a directory, EACCES) is answered in
111
+ // the tool's own words: the raw Node error carries the ABSOLUTE server
112
+ // path and a POSIX-only message, and the loop would wrap it as
113
+ // `Error: ENOENT: …`, which tells the model nothing actionable.
114
+ let buf;
115
+ try {
116
+ buf = await fs.readFile(file);
117
+ }
118
+ catch (e) {
119
+ const code = e.code;
120
+ if (code === 'ENOENT') {
121
+ return ('File not found: ' +
122
+ p +
123
+ ' (check the path; it must be inside the working directory)');
124
+ }
125
+ if (code === 'EISDIR') {
126
+ return (p +
127
+ ' is a directory, not a file. Use LS or Glob to list its contents.');
128
+ }
129
+ if (code === 'EACCES' || code === 'EPERM') {
130
+ return 'Permission denied reading ' + p;
131
+ }
132
+ return 'Error reading ' + p + ': ' + e.message;
133
+ }
111
134
  if (isImageName(file)) {
112
135
  return ('[image: ' +
113
136
  p +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.55",
3
+ "version": "2.82.56",
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": {