zames_pro 2.77.0 → 2.77.1
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 +10 -0
- package/dist/backlog.js +16 -1
- package/dist/qwen-net-capture.js +48 -1
- package/dist/sessions.js +19 -2
- package/dist/web.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.77.1] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **backlog:** only collapse real archive blocks, not notes that mention details
|
|
15
|
+
- **qwen:** do not read a retry phrase in the answer as a rate limit
|
|
16
|
+
- **web:** block IPv4-compatible IPv6 addresses in the SSRF guard
|
|
17
|
+
- **sessions:** cap the input history when loading it
|
|
18
|
+
- **sessions:** keep chat ids with unsafe characters in separate files
|
|
19
|
+
|
|
10
20
|
## [2.77.0] - 2026-10-17
|
|
11
21
|
|
|
12
22
|
### Added
|
package/dist/backlog.js
CHANGED
|
@@ -62,6 +62,20 @@ function isRealBoundary(line) {
|
|
|
62
62
|
function isDetailsOpen(line) {
|
|
63
63
|
return line.trim().startsWith('<details');
|
|
64
64
|
}
|
|
65
|
+
// An ARCHIVE block is a <details> that actually wraps the struck-through copy
|
|
66
|
+
// of an item heading (`### X. ~~original~~`). A note that merely MENTIONS
|
|
67
|
+
// `Use <details> in README:` must not be treated as an archive: collapsing it
|
|
68
|
+
// deleted the note's own body (everything up to the next heading).
|
|
69
|
+
function isArchiveBlock(lines, from) {
|
|
70
|
+
for (let j = from; j < lines.length; j++) {
|
|
71
|
+
const t = lines[j].trim();
|
|
72
|
+
if (isArchivedHeading(t))
|
|
73
|
+
return true;
|
|
74
|
+
if (t.startsWith('</details>') || isRealBoundary(t))
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
65
79
|
function isArchivedHeading(line) {
|
|
66
80
|
return /^###\s+[A-Z]\d+[.]\s+~~/.test(line.trim());
|
|
67
81
|
}
|
|
@@ -85,7 +99,8 @@ export function collapseBacklog(text) {
|
|
|
85
99
|
let collapsed = 0;
|
|
86
100
|
let i = 0;
|
|
87
101
|
while (i < lines.length) {
|
|
88
|
-
|
|
102
|
+
const archiveOpen = isDetailsOpen(lines[i]) && isArchiveBlock(lines, i + 1);
|
|
103
|
+
if (archiveOpen || isArchivedHeading(lines[i])) {
|
|
89
104
|
// Skip to the end of the archive: the first `</details>` OR the first
|
|
90
105
|
// real heading, whichever comes first (see the note above).
|
|
91
106
|
let j = i + 1;
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -195,6 +195,12 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
195
195
|
}
|
|
196
196
|
// True when the body carries Qwen's rate-limit / busy notice, so ask() can
|
|
197
197
|
// back off instead of waiting out the deadline. Matched against the RAW body.
|
|
198
|
+
// The classifier runs against the RAW body, so a phrase like `please try again
|
|
199
|
+
// later` would also match a NORMAL answer (the model writing about retry
|
|
200
|
+
// logic) and make ask() back off on a good turn. A real notice arrives in an
|
|
201
|
+
// ERROR payload (`event: error` / `"type":"error"`) or as a code; answer text
|
|
202
|
+
// lives in `choices[].delta.content`. We therefore only inspect the body when
|
|
203
|
+
// it is NOT a plain answer stream, or when it carries no answer content.
|
|
198
204
|
export function isRateLimitText(body) {
|
|
199
205
|
const b = String(body || '');
|
|
200
206
|
if (!b)
|
|
@@ -203,7 +209,42 @@ export function isRateLimitText(body) {
|
|
|
203
209
|
// ("The service is currently experiencing high demand. Please try again
|
|
204
210
|
// later.") — a TRANSIENT capacity limit, so it belongs with the rate-limit
|
|
205
211
|
// family (back off and retry), not with the guest DAILY limit.
|
|
206
|
-
|
|
212
|
+
if (/quota_limit/i.test(b))
|
|
213
|
+
return true;
|
|
214
|
+
if (/event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b)) {
|
|
215
|
+
return /too many requests|rate limit|too frequent|please try again later|high demand|слишком часто/i.test(b);
|
|
216
|
+
}
|
|
217
|
+
// No structured error marker: only treat the body as a notice when it has
|
|
218
|
+
// NO answer content (a bare notice page/JSON).
|
|
219
|
+
if (hasAnswerContent(b))
|
|
220
|
+
return false;
|
|
221
|
+
return /too many requests|rate limit|too frequent|please try again later|high demand|слишком часто/i.test(b);
|
|
222
|
+
}
|
|
223
|
+
// True when the body carries actual answer text in an SSE/JSON chunk.
|
|
224
|
+
function hasAnswerContent(body) {
|
|
225
|
+
for (const obj of parseDataLines(body)) {
|
|
226
|
+
const choices = obj.choices;
|
|
227
|
+
if (!Array.isArray(choices))
|
|
228
|
+
continue;
|
|
229
|
+
for (const ch of choices) {
|
|
230
|
+
if (!ch || typeof ch !== 'object')
|
|
231
|
+
continue;
|
|
232
|
+
const c = ch;
|
|
233
|
+
const delta = c.delta && typeof c.delta === 'object'
|
|
234
|
+
? c.delta
|
|
235
|
+
: c.message && typeof c.message === 'object'
|
|
236
|
+
? c.message
|
|
237
|
+
: null;
|
|
238
|
+
if (!delta)
|
|
239
|
+
continue;
|
|
240
|
+
if (isThinkingPhase(delta))
|
|
241
|
+
continue;
|
|
242
|
+
if (typeof delta.content === 'string' && delta.content.trim()) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
207
248
|
}
|
|
208
249
|
// The GUEST daily limit is a DIFFERENT failure from a transient rate limit:
|
|
209
250
|
// Qwen answers `RateLimited` with "You've reached the guest chat limit for
|
|
@@ -221,5 +262,11 @@ export function isServerBusyText(body) {
|
|
|
221
262
|
const b = String(body || '');
|
|
222
263
|
if (!b)
|
|
223
264
|
return false;
|
|
265
|
+
// Same rule as isRateLimitText: a normal ANSWER may mention "try again
|
|
266
|
+
// later"; only a structured error payload or a body with NO answer content
|
|
267
|
+
// can be a server-busy notice.
|
|
268
|
+
const errNotice = /event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b);
|
|
269
|
+
if (!errNotice && hasAnswerContent(b))
|
|
270
|
+
return false;
|
|
224
271
|
return /server is busy|service unavailable|try again later/i.test(b);
|
|
225
272
|
}
|
package/dist/sessions.js
CHANGED
|
@@ -46,8 +46,21 @@ export function isInternalChat(id) {
|
|
|
46
46
|
function ensureDir() {
|
|
47
47
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
48
48
|
}
|
|
49
|
+
// A chat id is used as a FILE NAME. Replacing every unsafe character with `_`
|
|
50
|
+
// made two DIFFERENT ids collide on one file ('a/b' and 'a_b'), so the second
|
|
51
|
+
// session overwrote the first and the other chat could not be resumed. Encode
|
|
52
|
+
// the unsafe characters instead: `_` becomes `_5f` and every other unsafe byte
|
|
53
|
+
// `_XX` (hex), which is injective and still path-safe.
|
|
49
54
|
function safeName(id) {
|
|
50
|
-
|
|
55
|
+
const s = String(id);
|
|
56
|
+
let out = '';
|
|
57
|
+
for (const ch of s) {
|
|
58
|
+
if (/[a-zA-Z0-9.-]/.test(ch))
|
|
59
|
+
out += ch;
|
|
60
|
+
else
|
|
61
|
+
out += '_' + ch.codePointAt(0).toString(16).padStart(2, '0');
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
51
64
|
}
|
|
52
65
|
// Save/update a session. workdir is needed so that when launched from the same
|
|
53
66
|
// project we restore exactly its last chat.
|
|
@@ -194,7 +207,11 @@ export function loadHistory() {
|
|
|
194
207
|
const raw = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
195
208
|
if (!Array.isArray(raw))
|
|
196
209
|
return [];
|
|
197
|
-
|
|
210
|
+
const list = raw.filter((x) => typeof x === 'string');
|
|
211
|
+
// Cap on LOAD as well: an old/hand-edited/merged file could exceed the
|
|
212
|
+
// limit, and the next save would write the whole array back out. Keep the
|
|
213
|
+
// most recent (tail) entries, matching saveHistory().
|
|
214
|
+
return list.length > HISTORY_LIMIT ? list.slice(-HISTORY_LIMIT) : list;
|
|
198
215
|
}
|
|
199
216
|
catch {
|
|
200
217
|
return [];
|
package/dist/web.js
CHANGED
|
@@ -188,6 +188,26 @@ export function isPrivateIp(ip) {
|
|
|
188
188
|
}
|
|
189
189
|
if (low.startsWith('64:ff9b:'))
|
|
190
190
|
return true;
|
|
191
|
+
// IPv4-COMPATIBLE form `::a.b.c.d` (deprecated, but the OS/HTTP stack still
|
|
192
|
+
// resolves it, so `http://[::127.0.0.1]/` reached localhost). `::` itself and
|
|
193
|
+
// `::1` are already handled above; `::ffff:…` was handled as MAPPED. Any
|
|
194
|
+
// other `::`-prefixed address whose tail is an embedded IPv4 belongs to the
|
|
195
|
+
// same family and must be judged by the embedded address.
|
|
196
|
+
const compat = low.match(/^::(?:ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
197
|
+
if (compat)
|
|
198
|
+
return isPrivateIp(compat[1]);
|
|
199
|
+
const compatHex = low.match(/^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
200
|
+
if (compatHex) {
|
|
201
|
+
const hi = parseInt(compatHex[1], 16);
|
|
202
|
+
const lo = parseInt(compatHex[2], 16);
|
|
203
|
+
return isPrivateIp(((hi >> 8) & 0xff) +
|
|
204
|
+
'.' +
|
|
205
|
+
(hi & 0xff) +
|
|
206
|
+
'.' +
|
|
207
|
+
((lo >> 8) & 0xff) +
|
|
208
|
+
'.' +
|
|
209
|
+
(lo & 0xff));
|
|
210
|
+
}
|
|
191
211
|
return false;
|
|
192
212
|
}
|
|
193
213
|
export function isPrivateHostname(host) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.77.
|
|
3
|
+
"version": "2.77.1",
|
|
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": {
|