zames_pro 2.43.0 → 2.45.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/dist/agent-loop.js +14 -4
- package/dist/attachments.js +11 -4
- package/dist/browser.js +57 -15
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -773,10 +773,20 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
773
773
|
export function truncateToolResult(text, limit) {
|
|
774
774
|
if (text.length <= limit)
|
|
775
775
|
return text;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
776
|
+
// Prefer a LINE boundary near the limit so the model does not read a line
|
|
777
|
+
// cut in half (a half line is easy to misread as the real content). We look
|
|
778
|
+
// for the last newline within the last 20% of the window; if none, fall back
|
|
779
|
+
// to the hard char cut.
|
|
780
|
+
let cut = limit;
|
|
781
|
+
const NL = String.fromCharCode(10);
|
|
782
|
+
const searchFrom = Math.floor(limit * 0.8);
|
|
783
|
+
const nl = text.lastIndexOf(NL, limit);
|
|
784
|
+
// Cut AFTER the newline (include it) so the kept part is a whole number of
|
|
785
|
+
// lines — a cut that EXCLUDED the newline left a half line before the marker.
|
|
786
|
+
if (nl >= searchFrom)
|
|
787
|
+
cut = nl + 1;
|
|
788
|
+
const omitted = text.length - cut;
|
|
789
|
+
return text.slice(0, cut) + NL + `[...truncated ${omitted} chars]`;
|
|
780
790
|
}
|
|
781
791
|
// The answer looks like a tool call, but parseToolCall() did not recognize it.
|
|
782
792
|
// Used as a safeguard against "the agent called a tool and stopped": in that
|
package/dist/attachments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
4
5
|
import { execFileSync } from 'child_process';
|
|
5
6
|
import { extractPathToken } from './path-token.js';
|
|
6
7
|
export { extractPathToken };
|
|
@@ -15,6 +16,11 @@ export const IMAGE_EXTS = [
|
|
|
15
16
|
'.ico',
|
|
16
17
|
];
|
|
17
18
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
19
|
+
// Stable content hash for de-duplicating identical attachments (a pasted image
|
|
20
|
+
// is saved to a NEW temp file every time, so its path is not a stable identity).
|
|
21
|
+
export function contentHash(data) {
|
|
22
|
+
return createHash('sha1').update(data).digest('hex');
|
|
23
|
+
}
|
|
18
24
|
function sanitize(name) {
|
|
19
25
|
return (String(name || '')
|
|
20
26
|
.replace(/[\u0000-\u001f\u007f]/g, '')
|
|
@@ -139,10 +145,10 @@ export class AttachmentStore {
|
|
|
139
145
|
this.fileCount = 0;
|
|
140
146
|
}
|
|
141
147
|
add(item) {
|
|
142
|
-
// De-duplicate by real path
|
|
143
|
-
//
|
|
144
|
-
// The operator saw
|
|
145
|
-
const existing = this.items.find((a) => a.path === item.path);
|
|
148
|
+
// De-duplicate by real path (files/paths) OR by content hash (images and
|
|
149
|
+
// clipboard data, which are re-saved to a fresh temp file on every paste).
|
|
150
|
+
// The operator saw the SAME file become [file#1] and [file#2].
|
|
151
|
+
const existing = this.items.find((a) => a.path === item.path || (item.hash && a.hash && a.hash === item.hash));
|
|
146
152
|
if (existing)
|
|
147
153
|
return existing;
|
|
148
154
|
const image = isImageName(item.name) || String(item.mime).startsWith('image/');
|
|
@@ -156,6 +162,7 @@ export class AttachmentStore {
|
|
|
156
162
|
image,
|
|
157
163
|
marker,
|
|
158
164
|
size: Number(item.size) || 0,
|
|
165
|
+
hash: item.hash,
|
|
159
166
|
};
|
|
160
167
|
this.items.push(att);
|
|
161
168
|
return att;
|
package/dist/browser.js
CHANGED
|
@@ -2124,28 +2124,70 @@ export class DeepSeekBrowser {
|
|
|
2124
2124
|
finalPrompt = rewriteFailedAttachments(finalPrompt, failed);
|
|
2125
2125
|
}
|
|
2126
2126
|
await this._setInputText(input, finalPrompt);
|
|
2127
|
-
// Send
|
|
2128
|
-
//
|
|
2129
|
-
//
|
|
2127
|
+
// Send: wait for the send button to become ENABLED first. After an
|
|
2128
|
+
// attachment upload the button is DISABLED while the file is still
|
|
2129
|
+
// uploading; a click on a disabled button silently does nothing, so the
|
|
2130
|
+
// message stayed in the input and no generation started (the operator saw
|
|
2131
|
+
// the text + the file in the box, but nothing was sent). We poll for an
|
|
2132
|
+
// ENABLED button, then click; if none becomes enabled in time, fall back
|
|
2133
|
+
// to Enter (which is also ignored while the send is disabled).
|
|
2134
|
+
const sendDeadline = Date.now() + 8000;
|
|
2130
2135
|
let sent = false;
|
|
2131
|
-
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2136
|
+
while (!sent && Date.now() < sendDeadline) {
|
|
2137
|
+
for (const sel of SEND_SELECTORS) {
|
|
2138
|
+
const btn = this.page.locator(sel).last();
|
|
2139
|
+
try {
|
|
2140
|
+
if ((await btn.count()) === 0)
|
|
2141
|
+
continue;
|
|
2142
|
+
if (!(await btn.isVisible().catch(() => false)))
|
|
2143
|
+
continue;
|
|
2144
|
+
// DeepSeek's send control is a div[role=button]; Playwright's
|
|
2145
|
+
// isEnabled() does NOT honor aria-disabled on a div, so read the
|
|
2146
|
+
// attribute (and a disabled class) directly — during an upload the
|
|
2147
|
+
// button is aria-disabled=true and a click is silently ignored.
|
|
2148
|
+
const enabled = await btn
|
|
2149
|
+
.evaluate((el) => {
|
|
2150
|
+
if (el.getAttribute('aria-disabled') === 'true')
|
|
2151
|
+
return false;
|
|
2152
|
+
const cls = (el.className || '').toString();
|
|
2153
|
+
if (/disabled|is-disabled/.test(cls))
|
|
2154
|
+
return false;
|
|
2155
|
+
return true;
|
|
2156
|
+
})
|
|
2157
|
+
.catch(() => true);
|
|
2158
|
+
if (!enabled)
|
|
2159
|
+
continue;
|
|
2160
|
+
await btn.click({ timeout: 1500 });
|
|
2161
|
+
sent = true;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
catch { }
|
|
2141
2165
|
}
|
|
2142
|
-
|
|
2166
|
+
if (!sent)
|
|
2167
|
+
await this.page.waitForTimeout(150);
|
|
2143
2168
|
}
|
|
2144
2169
|
if (!sent) {
|
|
2145
2170
|
await this.page.keyboard.press('Enter');
|
|
2146
2171
|
}
|
|
2147
2172
|
this._lastSentAt = Date.now();
|
|
2148
|
-
this._askDebug('SENT at=' + this._lastSentAt);
|
|
2173
|
+
this._askDebug('SENT at=' + this._lastSentAt + ' viaButton=' + sent);
|
|
2174
|
+
// Verify the send REALLY went out: DeepSeek clears the input when the
|
|
2175
|
+
// message is accepted. If the text is still there a moment later, the
|
|
2176
|
+
// click/Enter was ignored (a disabled button during upload, a lost
|
|
2177
|
+
// keystroke) — press Enter once more so the message is not silently stuck
|
|
2178
|
+
// in the box (exactly the operator's "text + file in the box, no send").
|
|
2179
|
+
try {
|
|
2180
|
+
await this.page.waitForTimeout(300);
|
|
2181
|
+
const stillThere = await input
|
|
2182
|
+
.evaluate((el) => (el.value || el.innerText || el.textContent || '').trim())
|
|
2183
|
+
.catch(() => '');
|
|
2184
|
+
if (stillThere) {
|
|
2185
|
+
this._askDebug('SEND not cleared, pressing Enter again');
|
|
2186
|
+
await this.page.keyboard.press('Enter');
|
|
2187
|
+
this._lastSentAt = Date.now();
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
catch { }
|
|
2149
2191
|
// Wait for the start: either Stop appeared, or the answer text changed,
|
|
2150
2192
|
// or the page grew (a new answer node / longer last answer). In parallel we
|
|
2151
2193
|
// catch the rate-limit toast (only toasts, not the whole body).
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createTools } from './tools.js';
|
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
11
11
|
import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
|
|
12
|
-
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
12
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, contentHash, } from './attachments.js';
|
|
13
13
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
14
14
|
import { runConfigMenu } from './config-menu.js';
|
|
15
15
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -1506,6 +1506,7 @@ async function main() {
|
|
|
1506
1506
|
name,
|
|
1507
1507
|
mime: image.mime,
|
|
1508
1508
|
size: image.data.length,
|
|
1509
|
+
hash: contentHash(image.data),
|
|
1509
1510
|
});
|
|
1510
1511
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1511
1512
|
marker: att.marker,
|
|
@@ -1579,6 +1580,7 @@ async function main() {
|
|
|
1579
1580
|
name,
|
|
1580
1581
|
mime,
|
|
1581
1582
|
size: res.data.length,
|
|
1583
|
+
hash: contentHash(res.data),
|
|
1582
1584
|
});
|
|
1583
1585
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1584
1586
|
marker: att.marker,
|