zames_pro 2.81.22 → 2.82.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 +18 -0
- package/dist/agent-loop.js +15 -1
- package/dist/browser-pure.js +18 -1
- package/dist/browser.js +55 -3
- package/dist/commands.js +5 -0
- package/dist/compact.js +33 -12
- package/dist/context-limit.js +25 -0
- package/dist/deepseek-ui.js +10 -0
- package/dist/glm-browser.js +56 -2
- package/dist/glm-net-capture.js +20 -0
- package/dist/i18n/deepseek.js +12 -0
- package/dist/i18n/glm.js +12 -0
- package/dist/i18n/qwen.js +12 -0
- package/dist/index.js +53 -3
- package/dist/qwen-browser.js +63 -2
- package/dist/qwen-net-capture.js +18 -0
- package/dist/run-task.js +2 -1
- package/dist/shell.js +10 -0
- package/dist/tui/FullScreenApp.js +23 -3
- package/dist/tui/log-window.js +15 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.82.1] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **shell:** an empty command answers instead of throwing
|
|
15
|
+
|
|
16
|
+
## [2.82.0] - 2026-10-22
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **compact:** recover from an exhausted chat context on every provider
|
|
21
|
+
|
|
22
|
+
## [2.81.23] - 2026-10-22
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **resume:** a respond message carrying the safety block is hidden
|
|
27
|
+
|
|
10
28
|
## [2.81.22] - 2026-10-22
|
|
11
29
|
|
|
12
30
|
### Fixed
|
package/dist/agent-loop.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
2
|
+
import { isContextLimitText } from './browser-pure.js';
|
|
2
3
|
import { providerLabel as providerHumanLabel } from './providers.js';
|
|
3
4
|
import { loadProjectContext } from './context.js';
|
|
4
5
|
import { getGitContext, formatGitContext } from './gitTools.js';
|
|
@@ -7,7 +8,7 @@ import { substituteAttachmentMarkers } from './commands.js';
|
|
|
7
8
|
import { loadHooks, runPreToolUse, runPostToolUse, runLifecycleHooks, } from './hooks.js';
|
|
8
9
|
import { loadPermissions, decidePermission, } from './permissions.js';
|
|
9
10
|
import { shouldRunDiagnostics, runDiagnostics, } from './diagnostics.js';
|
|
10
|
-
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
11
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, onContextLimit = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
11
12
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
12
13
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
13
14
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
@@ -48,6 +49,15 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
48
49
|
// editor's repaint, so the operator only saw the spinner. `onNotice` prints
|
|
49
50
|
// the notice ABOVE the input line.
|
|
50
51
|
browser.onNotice = safe(onNotice);
|
|
52
|
+
// The chat ran out of context and refuses further sends ("Length limit
|
|
53
|
+
// reached. Please start a new chat."). The browser cannot recover by itself:
|
|
54
|
+
// compaction needs the system prompt, the tools and the session bookkeeping
|
|
55
|
+
// that live above it, so the callback is attached here, where the caller's
|
|
56
|
+
// hook lives (the same seam pattern as onAutoCompact).
|
|
57
|
+
if ('onContextLimit' in browser) {
|
|
58
|
+
;
|
|
59
|
+
browser.onContextLimit = onContextLimit;
|
|
60
|
+
}
|
|
51
61
|
const safeAssistantThought = safe(onAssistantThought);
|
|
52
62
|
const safeToolCall = safe(onToolCall);
|
|
53
63
|
const safeToolResult = safe(onToolResult);
|
|
@@ -494,6 +504,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
494
504
|
// tool-call and not a real final answer — treat it as a service
|
|
495
505
|
// answer so the loop re-asks instead of stopping on it.
|
|
496
506
|
/<ds_safety>|<\/ds_safety>/i.test(trimmed) ||
|
|
507
|
+
// An exhausted-context notice can also arrive as the turn's TEXT (it
|
|
508
|
+
// is short and carries no tool call). Treat it as a service answer so
|
|
509
|
+
// the loop re-asks instead of finishing on "start a new chat".
|
|
510
|
+
isContextLimitText(trimmed) ||
|
|
497
511
|
(trimmed.length <= 200 &&
|
|
498
512
|
/(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
|
|
499
513
|
if (looksService && stallRetries < MAX_STALL_RETRIES) {
|
package/dist/browser-pure.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { chromium } from 'playwright';
|
|
5
|
-
import { GENERATION_ERR_RE, GENERATION_TIMEOUT_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, STATUS_RE, } from './deepseek-ui.js';
|
|
5
|
+
import { CONTEXT_LIMIT_RE, GENERATION_ERR_RE, GENERATION_TIMEOUT_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, STATUS_RE, } from './deepseek-ui.js';
|
|
6
6
|
// Drop service placeholders ("Reading…", "Thinking…") so they are not mistaken
|
|
7
7
|
// for an answer; keep everything else as-is (including the whitespace the
|
|
8
8
|
// tool-call relies on). Pure: no `this`, no page.
|
|
@@ -283,6 +283,13 @@ export function isGenerationIncompleteText(text) {
|
|
|
283
283
|
export function isRateLimitText(text) {
|
|
284
284
|
return RATE_LIMIT_RE.test(String(text || ''));
|
|
285
285
|
}
|
|
286
|
+
// The chat ran out of context: DeepSeek answers "Length limit reached. Please
|
|
287
|
+
// start a new chat." and will refuse every further send into this chat. Distinct
|
|
288
|
+
// from the rate limit (which clears with time) and from a server hiccup — the
|
|
289
|
+
// only recovery is compaction, so ask() must not retry in the same chat.
|
|
290
|
+
export function isContextLimitText(text) {
|
|
291
|
+
return CONTEXT_LIMIT_RE.test(String(text || ''));
|
|
292
|
+
}
|
|
286
293
|
export function isServerBusyText(text) {
|
|
287
294
|
const t = String(text || '');
|
|
288
295
|
// Rate limit takes priority (it needs the long wait).
|
|
@@ -313,6 +320,16 @@ export class ServerBusyError extends Error {
|
|
|
313
320
|
this.name = 'ServerBusyError';
|
|
314
321
|
}
|
|
315
322
|
}
|
|
323
|
+
// The chat's context window is exhausted ("Length limit reached. Please start
|
|
324
|
+
// a new chat."). The server rejects EVERY send into this chat, so a retry in
|
|
325
|
+
// place is pointless: the caller must compact (open a NEW chat with a handover
|
|
326
|
+
// summary) and only then resend.
|
|
327
|
+
export class ContextLimitError extends Error {
|
|
328
|
+
constructor(detail) {
|
|
329
|
+
super('Chat context limit reached. ' + detail);
|
|
330
|
+
this.name = 'ContextLimitError';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
316
333
|
// The generation was truncated by the server (`generation_err` / INCOMPLETE).
|
|
317
334
|
// Distinct from ServerBusyError: the DeepSeek backend answered, but the turn
|
|
318
335
|
// did not finish, and the UI offers a "Continue" button. The agent retries the
|
package/dist/browser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { chromium, } from 'playwright';
|
|
|
2
2
|
import { interactiveBrowserLogin, loginIsInteractive, canShowWindow, } from './browser-login.js';
|
|
3
3
|
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, isFinishedWithAnswer, } from './net-capture.js';
|
|
4
4
|
import { rewriteFailedAttachments } from './commands.js';
|
|
5
|
+
import { recoverFromContextLimit } from './context-limit.js';
|
|
5
6
|
import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
|
|
6
7
|
import { parseHistoryMessages, extractChatMessages, scrapeChatList, scrapeChatMessages, } from './browser-chats.js';
|
|
7
8
|
import { CHAT_URL, CHAT_ID_RE, chatUrl, HISTORY_MESSAGES_URL, BUTTON_CLASS_RE, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, CONTINUE_NAME_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, RESTORE_ANSWER_SELECTORS, ASSISTANT_CLASS_RE, ASSISTANT_SELECTOR, ANSWER_MARKDOWN_SELECTOR, MARKDOWN_SELECTOR, CHAT_SIGNAL_NODES_SELECTOR, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, PASSWORD_SELECTORS, LOGIN_SELECTORS, LOGIN_SUBMIT_SELECTORS, MFA_CODE_SELECTORS, MFA_SUBMIT_SELECTORS, MFA_SEND_CODE_SELECTORS, MFA_SEND_CODE_RE, LOGIN_BUTTON_SELECTOR, LOGIN_NAME_RE, LOGIN_FORM_SUBMIT_RE, SIDEBAR_TOGGLE_SELECTORS, TOGGLE_SELECTORS, DEEP_THINKING_RE, WEB_SEARCH_RE, ATTACH_SELECTORS, ANSWER_URL_RE, } from './deepseek-ui.js';
|
|
@@ -21,8 +22,8 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
|
|
|
21
22
|
// imported for the class below and RE-EXPORTED here, so the public API of this
|
|
22
23
|
// module is unchanged (tests and other modules import these names from
|
|
23
24
|
// './browser.js').
|
|
24
|
-
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
25
|
-
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, };
|
|
25
|
+
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
26
|
+
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, };
|
|
26
27
|
export class DeepSeekBrowser {
|
|
27
28
|
providerName = 'deepseek';
|
|
28
29
|
headless;
|
|
@@ -40,6 +41,11 @@ export class DeepSeekBrowser {
|
|
|
40
41
|
rateLimitWaitMs;
|
|
41
42
|
maxRateLimitRetries;
|
|
42
43
|
maxServerBusyRetries;
|
|
44
|
+
// Ask() budget for the context-limit recovery. A compaction that does not
|
|
45
|
+
// free the window leaves the chat just as rejected as before, so the
|
|
46
|
+
// recovery is capped independently of askRetries.
|
|
47
|
+
maxContextLimitRetries;
|
|
48
|
+
onContextLimit;
|
|
43
49
|
serverBusyWaitMs;
|
|
44
50
|
// Retries for a turn the server truncated (generation_err / INCOMPLETE).
|
|
45
51
|
maxIncompleteRetries;
|
|
@@ -168,7 +174,7 @@ export class DeepSeekBrowser {
|
|
|
168
174
|
_windowClosed;
|
|
169
175
|
/** Session cookies captured before a relaunch (no `expires` -> in-memory). */
|
|
170
176
|
_carryCookies;
|
|
171
|
-
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, loginTimeoutMs = 600000, deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, onMfaPrompt, userAgent, } = {}) {
|
|
177
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, maxContextLimitRetries = 1, onContextLimit, autoContinue = true, continueMinGapMs = 1500, loginTimeoutMs = 600000, deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, onMfaPrompt, userAgent, } = {}) {
|
|
172
178
|
this.headless = headless;
|
|
173
179
|
this.debug = debug;
|
|
174
180
|
this.channel = channel;
|
|
@@ -184,6 +190,8 @@ export class DeepSeekBrowser {
|
|
|
184
190
|
this.serverBusyWaitMs = serverBusyWaitMs;
|
|
185
191
|
this.maxIncompleteRetries = maxIncompleteRetries;
|
|
186
192
|
this.incompleteWaitMs = incompleteWaitMs;
|
|
193
|
+
this.maxContextLimitRetries = maxContextLimitRetries;
|
|
194
|
+
this.onContextLimit = onContextLimit || null;
|
|
187
195
|
this.autoContinue = autoContinue;
|
|
188
196
|
this.continueMinGapMs = continueMinGapMs;
|
|
189
197
|
this.loginTimeoutMs = loginTimeoutMs;
|
|
@@ -1594,6 +1602,10 @@ export class DeepSeekBrowser {
|
|
|
1594
1602
|
// hiccup, not a failed send, and it is more likely in the reasoning
|
|
1595
1603
|
// (deep thinking) mode, where the long THINK phase gets cut off.
|
|
1596
1604
|
let incompleteRetries = 0;
|
|
1605
|
+
// Bumped when the chat's context window is exhausted. Recovery is a
|
|
1606
|
+
// COMPACTION (a new chat), not a resend, so it has its own budget: a
|
|
1607
|
+
// compaction that frees nothing leaves the chat just as rejected.
|
|
1608
|
+
let contextLimitRetries = 0;
|
|
1597
1609
|
// Mark that OUR code has an ask in flight. A completion/continue body
|
|
1598
1610
|
// that arrives with _askInFlight === 0 was the operator's own turn, and
|
|
1599
1611
|
// _onResponse uses that to bump _lastSentAt so the next agent send waits
|
|
@@ -1693,6 +1705,34 @@ export class DeepSeekBrowser {
|
|
|
1693
1705
|
return '(прервано пользователем)';
|
|
1694
1706
|
continue;
|
|
1695
1707
|
}
|
|
1708
|
+
// The chat's context window is exhausted: every further send into
|
|
1709
|
+
// this chat is refused ("Length limit reached. Please start a new
|
|
1710
|
+
// chat."). Retrying here is useless — the caller compacts the chat
|
|
1711
|
+
// and opens a fresh one, then the SAME prompt is resent there. The
|
|
1712
|
+
// resend does not consume a regular ask() attempt.
|
|
1713
|
+
if (e instanceof ContextLimitError) {
|
|
1714
|
+
contextLimitRetries++;
|
|
1715
|
+
const res = await recoverFromContextLimit({
|
|
1716
|
+
recover: () => this.onContextLimit?.() ?? Promise.resolve(false),
|
|
1717
|
+
maxRetries: this.maxContextLimitRetries,
|
|
1718
|
+
attempt: contextLimitRetries,
|
|
1719
|
+
notice: (text) => this._notice(text),
|
|
1720
|
+
t: (k, p) => this._t(k, p),
|
|
1721
|
+
keys: {
|
|
1722
|
+
compact: 'ds.context_limit_compact',
|
|
1723
|
+
compactFailed: 'ds.context_limit_compact_failed',
|
|
1724
|
+
giveUp: 'ds.context_limit_give_up',
|
|
1725
|
+
},
|
|
1726
|
+
provider: 'DeepSeek',
|
|
1727
|
+
}, e);
|
|
1728
|
+
if (!res.resend)
|
|
1729
|
+
throw res.error || e;
|
|
1730
|
+
// The compaction has already typed into the new chat, so the send
|
|
1731
|
+
// throttle must count from that moment. Leaving `_lastSentAt` at
|
|
1732
|
+
// the failed send's time adds nothing: that send never went out.
|
|
1733
|
+
this._lastSentAt = Date.now();
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1696
1736
|
this._notice(theme.warn(this._t('ds.ask_retry', {
|
|
1697
1737
|
attempt: attempt + 1,
|
|
1698
1738
|
max: this.askRetries,
|
|
@@ -2085,6 +2125,12 @@ export class DeepSeekBrowser {
|
|
|
2085
2125
|
if (this._abort || this._askCancelled)
|
|
2086
2126
|
return '(прервано пользователем)';
|
|
2087
2127
|
const pageText = await this._readPageText();
|
|
2128
|
+
// Checked BEFORE the rate limit and the server hiccup: DeepSeek shows the
|
|
2129
|
+
// context notice INSTEAD of an answer, and both of those branches would
|
|
2130
|
+
// retry the same send into a chat that can never accept it again.
|
|
2131
|
+
if (isContextLimitText(pageText)) {
|
|
2132
|
+
throw new ContextLimitError(pageText.slice(0, 300));
|
|
2133
|
+
}
|
|
2088
2134
|
if (isRateLimitText(pageText)) {
|
|
2089
2135
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
2090
2136
|
}
|
|
@@ -2315,6 +2361,12 @@ export class DeepSeekBrowser {
|
|
|
2315
2361
|
if (this._netRateLimited && this._netCaptureAt >= this._lastSentAt) {
|
|
2316
2362
|
throw new RateLimitError(this._netBody.slice(0, 300) || 'rate limit (from network)');
|
|
2317
2363
|
}
|
|
2364
|
+
// The exhausted context can likewise be visible only in the captured
|
|
2365
|
+
// body rather than in a toast, so ask() must see it here as well.
|
|
2366
|
+
if (this._netCaptureAt >= this._lastSentAt &&
|
|
2367
|
+
isContextLimitText(this._netBody)) {
|
|
2368
|
+
throw new ContextLimitError(this._netBody.slice(0, 300));
|
|
2369
|
+
}
|
|
2318
2370
|
// The server truncated the turn (generation_err / INCOMPLETE) or ended
|
|
2319
2371
|
// it with only reasoning. PREFER the Continue button: it RESUMES the
|
|
2320
2372
|
// same turn (`chat/continue`). Resending the prompt creates a NEW turn
|
package/dist/commands.js
CHANGED
|
@@ -487,6 +487,11 @@ export function normalizeRestoredMessage(m) {
|
|
|
487
487
|
const msg = (call.message || '').trim();
|
|
488
488
|
if (!msg)
|
|
489
489
|
return null;
|
|
490
|
+
// The safety-classification block can sit INSIDE the respond message; the
|
|
491
|
+
// plain-text path already drops it, and printing the marker as the answer
|
|
492
|
+
// is the same leak.
|
|
493
|
+
if (/<\/?ds_safety\b/i.test(msg))
|
|
494
|
+
return null;
|
|
490
495
|
return { role: 'assistant', text: msg };
|
|
491
496
|
}
|
|
492
497
|
// A plain-text answer (written without the respond wrapper) — show as-is.
|
package/dist/compact.js
CHANGED
|
@@ -15,7 +15,7 @@ export const COMPACT_FALLBACK_LIMIT = 40;
|
|
|
15
15
|
* The caller owns the browser and the throttle; this function only sends.
|
|
16
16
|
*/
|
|
17
17
|
export async function performCompact(opts) {
|
|
18
|
-
const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, selfImprovement = false, } = opts;
|
|
18
|
+
const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, selfImprovement = false, contextExhausted = false, } = opts;
|
|
19
19
|
const t = translate(locale);
|
|
20
20
|
const log = (msg) => {
|
|
21
21
|
if (!quiet)
|
|
@@ -51,10 +51,13 @@ export async function performCompact(opts) {
|
|
|
51
51
|
transcript?.log('hook_pre_compact', { output: out });
|
|
52
52
|
}
|
|
53
53
|
catch { }
|
|
54
|
-
// 1) Ask the OLD chat to summarize itself.
|
|
54
|
+
// 1) Ask the OLD chat to summarize itself. On an EXHAUSTED chat that send
|
|
55
|
+
// is refused ("Length limit reached"), so go straight to the local
|
|
56
|
+
// fallback below — retrying the refused send wastes minutes on the rate
|
|
57
|
+
// limit and can never produce a summary.
|
|
55
58
|
let summary = '';
|
|
56
59
|
let summaryErr = '';
|
|
57
|
-
for (let attempt = 1; attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
60
|
+
for (let attempt = 1; !contextExhausted && attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
58
61
|
let prompt = buildCompactPrompt(locale);
|
|
59
62
|
if (attempt > 1) {
|
|
60
63
|
prompt +=
|
|
@@ -132,15 +135,33 @@ export async function performCompact(opts) {
|
|
|
132
135
|
beforeTokens,
|
|
133
136
|
});
|
|
134
137
|
// 2) New chat + system prompt + the summary as the first message.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
// The recovery path (contextExhausted) is RE-ENTRANT: this compaction was
|
|
139
|
+
// started BY a refused send, so a refusal during these sends would call
|
|
140
|
+
// back into compaction while it is still running. Drop the callback for
|
|
141
|
+
// the duration — by now the exhausted chat is behind us and a refusal here
|
|
142
|
+
// means the compaction itself cannot proceed, which the caller reports.
|
|
143
|
+
const prevOnContextLimit = browser.onContextLimit;
|
|
144
|
+
if (contextExhausted && 'onContextLimit' in browser) {
|
|
145
|
+
;
|
|
146
|
+
browser.onContextLimit = null;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await browser.newChat();
|
|
150
|
+
await browser.ask(buildSystemPrompt({ workdir, tools, locale, selfImprovement }), {
|
|
151
|
+
timeout: 60_000,
|
|
152
|
+
agent: false,
|
|
153
|
+
});
|
|
154
|
+
await browser.ask(buildCompactCarryover(summary, task), {
|
|
155
|
+
timeout: 60_000,
|
|
156
|
+
agent: false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (contextExhausted && 'onContextLimit' in browser) {
|
|
161
|
+
;
|
|
162
|
+
browser.onContextLimit = prevOnContextLimit || null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
144
165
|
const newChatId = browser.getCurrentChatId
|
|
145
166
|
? await browser.getCurrentChatId()
|
|
146
167
|
: null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run ONE context-limit recovery. The caller resends only on `resend: true`;
|
|
3
|
+
* on `resend: false` it throws `error`, which is the ORIGINAL failure so the
|
|
4
|
+
* operator keeps the real reason (a wrapper would hide the server text).
|
|
5
|
+
*/
|
|
6
|
+
export async function recoverFromContextLimit(deps, original) {
|
|
7
|
+
const { recover, maxRetries, attempt, notice, t, keys, provider } = deps;
|
|
8
|
+
const giveUp = () => {
|
|
9
|
+
notice(t(keys.giveUp, { provider }));
|
|
10
|
+
return { resend: false, error: original };
|
|
11
|
+
};
|
|
12
|
+
if (!recover || attempt > maxRetries)
|
|
13
|
+
return giveUp();
|
|
14
|
+
notice(t(keys.compact, { provider }));
|
|
15
|
+
let recovered = false;
|
|
16
|
+
try {
|
|
17
|
+
recovered = await recover();
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
notice(t(keys.compactFailed, { error: e.message }));
|
|
21
|
+
}
|
|
22
|
+
if (!recovered)
|
|
23
|
+
return giveUp();
|
|
24
|
+
return { resend: true };
|
|
25
|
+
}
|
package/dist/deepseek-ui.js
CHANGED
|
@@ -243,6 +243,16 @@ export const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate lim
|
|
|
243
243
|
// rate limit, these usually clear in seconds, so we retry quickly instead of
|
|
244
244
|
// waiting minutes.
|
|
245
245
|
export const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
|
|
246
|
+
// The chat's CONTEXT WINDOW is exhausted. DeepSeek refuses to continue the
|
|
247
|
+
// conversation and answers with a non-retryable notice instead of a model
|
|
248
|
+
// turn: "Length limit reached. Please start a new chat." (the Russian UI:
|
|
249
|
+
// «Достигнут лимит длины» / «начните новый чат»). Retrying the same send can
|
|
250
|
+
// never succeed and every such attempt burned the askRetries budget, after
|
|
251
|
+
// which the agent stopped with a bare error — the operator had to notice the
|
|
252
|
+
// notice in the chat by hand. The only way out is compaction (a NEW chat with
|
|
253
|
+
// a handover summary), so this must be its own error class, not a rate limit
|
|
254
|
+
// or a server hiccup.
|
|
255
|
+
export const CONTEXT_LIMIT_RE = /(length limit (?:has been )?reached|context (?:window|length) (?:is )?(?:exceeded|full)|maximum context|start a new (?:chat|conversation)|достигнут лимит длин|лимит длин[ыа] (?:достигнут|исчерпан)|начните новый чат|начните нов(ый|ую) (диалог|бесед))/i;
|
|
246
256
|
// ---------- network protocol (SSE) ----------
|
|
247
257
|
export const GENERATION_ERR_RE = /"finish_reason":\s*"generation_err"/i;
|
|
248
258
|
// The server gave up on the turn ("Server busy, please try again later."):
|
package/dist/glm-browser.js
CHANGED
|
@@ -21,6 +21,7 @@ import os from 'os';
|
|
|
21
21
|
import { chromium } from 'playwright';
|
|
22
22
|
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
23
23
|
import { rewriteFailedAttachments } from './commands.js';
|
|
24
|
+
import { recoverFromContextLimit } from './context-limit.js';
|
|
24
25
|
import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
|
|
25
26
|
import { theme } from './theme.js';
|
|
26
27
|
import { scrapeGlmChatList, scrapeGlmChatMessages, parseGlmChatList, parseGlmHistory, } from './glm-chats.js';
|
|
@@ -28,7 +29,7 @@ import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadle
|
|
|
28
29
|
import { readLastAnswerInDom, readPageToastsInDom, readUserAgentInDom, readInputTrimmedTextInDom, isElementEnabledInDom, setBusyTitleInDom, chatSignalInDom, } from './browser-dom.js';
|
|
29
30
|
import { probeUiHealth, } from './ui-health.js';
|
|
30
31
|
import { CHAT_URL, LOGIN_URL, CHAT_ID_RE, chatUrl, INPUT_SELECTORS, UPLOAD_BUTTON_SELECTOR, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, NEW_CHAT_SELECTOR, MODELS_URL, MODEL_TRIGGER_SELECTOR, modelIdFromTriggerId, CHAT_LIST_URL, chatHistoryUrl, AUTH_URL, ANSWER_URL_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, USER_CLASS_RE, CHAT_SIGNAL_NODES_SELECTOR, MARKDOWN_SELECTOR, } from './glm-ui.js';
|
|
31
|
-
import { extractAnswer, extractTokenUsage, isFinishedWithoutAnswer, isRateLimitText, isQuotaExhaustedText, isServerBusyText, isGuestLimitText, isCaptchaText, extractChatId, } from './glm-net-capture.js';
|
|
32
|
+
import { extractAnswer, extractTokenUsage, isFinishedWithoutAnswer, isRateLimitText, isQuotaExhaustedText, isServerBusyText, isGuestLimitText, isContextLimitText, isCaptchaText, extractChatId, } from './glm-net-capture.js';
|
|
32
33
|
import { SendRuntime } from './send-runtime.js';
|
|
33
34
|
import { readGlmBodyFeatures, unsafeGlmModes, glmFlagsMismatch, } from './glm-flags.js';
|
|
34
35
|
import { interactiveBrowserLogin, loginIsInteractive, canShowWindow, } from './browser-login.js';
|
|
@@ -56,6 +57,9 @@ export class GlmBrowser {
|
|
|
56
57
|
serverBusyWaitMs;
|
|
57
58
|
maxIncompleteRetries;
|
|
58
59
|
incompleteWaitMs;
|
|
60
|
+
// Ask() budget for the context-limit recovery (a compaction, not a resend).
|
|
61
|
+
maxContextLimitRetries;
|
|
62
|
+
onContextLimit;
|
|
59
63
|
startWaitMs;
|
|
60
64
|
sendButtonWaitMs;
|
|
61
65
|
model;
|
|
@@ -81,6 +85,9 @@ export class GlmBrowser {
|
|
|
81
85
|
_netRateLimited;
|
|
82
86
|
_netGuestLimit;
|
|
83
87
|
_netServerBusy;
|
|
88
|
+
// True when the chat context window is exhausted: it refuses every
|
|
89
|
+
// further send, so ask() must compact into a fresh chat, not retry here.
|
|
90
|
+
_netContextLimit;
|
|
84
91
|
_netCaptcha;
|
|
85
92
|
_netChatId;
|
|
86
93
|
_lastTokenUsage;
|
|
@@ -112,7 +119,7 @@ export class GlmBrowser {
|
|
|
112
119
|
* removes most of the wait. Empty = leave the site's default.
|
|
113
120
|
*/
|
|
114
121
|
reasoningEffort;
|
|
115
|
-
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 300000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 0, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = false, auth, locale, onAuthSave, onLoginPrompt, disableAgentRuntime = true, reasoningEffort = 'low', sendTelemetry = true, logsDir, loginTimeoutMs = 600000, userAgent, } = {}) {
|
|
122
|
+
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 300000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 0, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, maxContextLimitRetries = 1, onContextLimit, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = false, auth, locale, onAuthSave, onLoginPrompt, disableAgentRuntime = true, reasoningEffort = 'low', sendTelemetry = true, logsDir, loginTimeoutMs = 600000, userAgent, } = {}) {
|
|
116
123
|
this.headless = headless;
|
|
117
124
|
this.debug = debug;
|
|
118
125
|
this.channel = channel;
|
|
@@ -127,6 +134,8 @@ export class GlmBrowser {
|
|
|
127
134
|
this.serverBusyWaitMs = serverBusyWaitMs;
|
|
128
135
|
this.maxIncompleteRetries = maxIncompleteRetries;
|
|
129
136
|
this.incompleteWaitMs = incompleteWaitMs;
|
|
137
|
+
this.maxContextLimitRetries = maxContextLimitRetries;
|
|
138
|
+
this.onContextLimit = onContextLimit || null;
|
|
130
139
|
this.startWaitMs = startWaitMs;
|
|
131
140
|
this.sendButtonWaitMs = sendButtonWaitMs;
|
|
132
141
|
this.model = model;
|
|
@@ -157,6 +166,7 @@ export class GlmBrowser {
|
|
|
157
166
|
this._netRateLimited = false;
|
|
158
167
|
this._netGuestLimit = false;
|
|
159
168
|
this._netServerBusy = false;
|
|
169
|
+
this._netContextLimit = false;
|
|
160
170
|
this._netCaptcha = false;
|
|
161
171
|
this._netChatId = null;
|
|
162
172
|
this._lastTokenUsage = null;
|
|
@@ -838,6 +848,10 @@ export class GlmBrowser {
|
|
|
838
848
|
if (isServerBusyText(body)) {
|
|
839
849
|
this._netServerBusy = true;
|
|
840
850
|
}
|
|
851
|
+
if (isContextLimitText(body)) {
|
|
852
|
+
this._netContextLimit = true;
|
|
853
|
+
this._netCapture = '';
|
|
854
|
+
}
|
|
841
855
|
this._netCaptureAt = Date.now();
|
|
842
856
|
}
|
|
843
857
|
}
|
|
@@ -1364,6 +1378,9 @@ export class GlmBrowser {
|
|
|
1364
1378
|
let rateLimitRetries = 0;
|
|
1365
1379
|
let serverBusyRetries = 0;
|
|
1366
1380
|
let incompleteRetries = 0;
|
|
1381
|
+
// The exhausted-context recovery is a COMPACTION (a new chat), not a
|
|
1382
|
+
// resend, so it has its own budget.
|
|
1383
|
+
let contextLimitRetries = 0;
|
|
1367
1384
|
this._askInFlight++;
|
|
1368
1385
|
try {
|
|
1369
1386
|
while (attempt < this.askRetries) {
|
|
@@ -1390,6 +1407,28 @@ export class GlmBrowser {
|
|
|
1390
1407
|
this.sendRuntime.onOutcome('rate_limit', { errorText: 'quota' });
|
|
1391
1408
|
throw new Error(this._t('glm.quota_exhausted'));
|
|
1392
1409
|
}
|
|
1410
|
+
// Checked BEFORE the rate-limit/captcha branches below: those
|
|
1411
|
+
// retry IN PLACE, which an exhausted chat can never accept.
|
|
1412
|
+
if (e instanceof GlmContextLimitError) {
|
|
1413
|
+
contextLimitRetries++;
|
|
1414
|
+
const res = await recoverFromContextLimit({
|
|
1415
|
+
recover: () => this.onContextLimit?.() ?? Promise.resolve(false),
|
|
1416
|
+
maxRetries: this.maxContextLimitRetries,
|
|
1417
|
+
attempt: contextLimitRetries,
|
|
1418
|
+
notice: (text) => this._notice(text),
|
|
1419
|
+
t: (k, p) => this._t(k, p),
|
|
1420
|
+
keys: {
|
|
1421
|
+
compact: 'glm.context_limit_compact',
|
|
1422
|
+
compactFailed: 'glm.context_limit_compact_failed',
|
|
1423
|
+
giveUp: 'glm.context_limit_give_up',
|
|
1424
|
+
},
|
|
1425
|
+
provider: 'GLM',
|
|
1426
|
+
}, e);
|
|
1427
|
+
if (!res.resend)
|
|
1428
|
+
throw res.error || e;
|
|
1429
|
+
this._lastSentAt = Date.now();
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1393
1432
|
if (e instanceof GlmCaptchaError) {
|
|
1394
1433
|
this.sendRuntime.onOutcome('guest_limit', { errorText: 'captcha' });
|
|
1395
1434
|
throw new Error(this._t('glm.captcha'));
|
|
@@ -1573,6 +1612,7 @@ export class GlmBrowser {
|
|
|
1573
1612
|
this._netRateLimited = false;
|
|
1574
1613
|
this._netGuestLimit = false;
|
|
1575
1614
|
this._netServerBusy = false;
|
|
1615
|
+
this._netContextLimit = false;
|
|
1576
1616
|
this._netCaptcha = false;
|
|
1577
1617
|
this._netBody = '';
|
|
1578
1618
|
await this._applyToggles();
|
|
@@ -1656,6 +1696,9 @@ export class GlmBrowser {
|
|
|
1656
1696
|
if (isRateLimitText(pageText)) {
|
|
1657
1697
|
throw new GlmRateLimitError(pageText.slice(0, 300));
|
|
1658
1698
|
}
|
|
1699
|
+
if (this._netContextLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
1700
|
+
throw new GlmContextLimitError(this._netBody.slice(0, 300));
|
|
1701
|
+
}
|
|
1659
1702
|
if (this._netCaptcha && this._netCaptureAt >= this._lastSentAt) {
|
|
1660
1703
|
throw new GlmCaptchaError(this._netBody.slice(0, 200));
|
|
1661
1704
|
}
|
|
@@ -1691,6 +1734,9 @@ export class GlmBrowser {
|
|
|
1691
1734
|
if (isRateLimitText(pageText)) {
|
|
1692
1735
|
throw new GlmRateLimitError(pageText.slice(0, 300));
|
|
1693
1736
|
}
|
|
1737
|
+
if (this._netContextLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
1738
|
+
throw new GlmContextLimitError(this._netBody.slice(0, 300));
|
|
1739
|
+
}
|
|
1694
1740
|
if (this._netCaptcha && this._netCaptureAt >= this._lastSentAt) {
|
|
1695
1741
|
throw new GlmCaptchaError(this._netBody.slice(0, 200));
|
|
1696
1742
|
}
|
|
@@ -1756,6 +1802,14 @@ class GlmCaptchaError extends Error {
|
|
|
1756
1802
|
this.name = 'GlmCaptchaError';
|
|
1757
1803
|
}
|
|
1758
1804
|
}
|
|
1805
|
+
// The chat context window is exhausted and the turn was refused. NOT
|
|
1806
|
+
// retryable in place: ask() compacts into a fresh chat and resends there.
|
|
1807
|
+
class GlmContextLimitError extends Error {
|
|
1808
|
+
constructor(detail) {
|
|
1809
|
+
super('context limit: ' + detail);
|
|
1810
|
+
this.name = 'GlmContextLimitError';
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1759
1813
|
class GlmIncompleteError extends Error {
|
|
1760
1814
|
constructor(detail) {
|
|
1761
1815
|
super('generation incomplete: ' + detail);
|
package/dist/glm-net-capture.js
CHANGED
|
@@ -345,3 +345,23 @@ export function extractChatId(body) {
|
|
|
345
345
|
const m2 = b.match(/"id"\s*:\s*"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/i);
|
|
346
346
|
return m2 ? m2[1] : null;
|
|
347
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* True when the RAW body says the chat's CONTEXT WINDOW is exhausted - the
|
|
350
|
+
* same hard failure as DeepSeek's length limit: every further send into that
|
|
351
|
+
* chat is refused, so the only recovery is a compaction into a fresh chat.
|
|
352
|
+
*
|
|
353
|
+
* Same guard as the other z.ai classifiers: the notice must be an ERROR
|
|
354
|
+
* payload (or carry no answer text). The model can legitimately WRITE about
|
|
355
|
+
* context limits, and treating such an answer as a refusal loses a good turn.
|
|
356
|
+
*/
|
|
357
|
+
export function isContextLimitText(body) {
|
|
358
|
+
const b = String(body || '');
|
|
359
|
+
if (!b)
|
|
360
|
+
return false;
|
|
361
|
+
const explicit = /context (?:length|window) (?:is )?(?:exceeded|too long|limit)|maximum context length|exceeds? the (?:maximum |model'?s )?context|too many (?:input )?tokens|length limit (?:has been )?reached|start a new (?:chat|conversation)|лимит длин|превышен[ао]? (?:максимальн[аяу]? )?(?:длина|контекст)|начните новый чат/i.test(b);
|
|
362
|
+
if (!explicit)
|
|
363
|
+
return false;
|
|
364
|
+
if (!ERROR_PAYLOAD_RE.test(b) && hasAnswerContent(b))
|
|
365
|
+
return false;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
package/dist/i18n/deepseek.js
CHANGED
|
@@ -56,6 +56,18 @@ export const deepseekMessages = {
|
|
|
56
56
|
ru: '✖ Не удалось получить ответ от {provider} после {max} попыток: {error}',
|
|
57
57
|
en: '✖ Could not get an answer from {provider} after {max} attempts: {error}',
|
|
58
58
|
},
|
|
59
|
+
'ds.context_limit_compact': {
|
|
60
|
+
ru: '🗜️ {provider}: лимит длины чата исчерпан («Length limit reached») — сжимаю историю и продолжаю в новом чате...',
|
|
61
|
+
en: '🗜️ {provider}: the chat hit its length limit ("Length limit reached") — compacting the history and continuing in a fresh chat...',
|
|
62
|
+
},
|
|
63
|
+
'ds.context_limit_compact_failed': {
|
|
64
|
+
ru: '⚠ Не удалось сжать историю: {error}',
|
|
65
|
+
en: '⚠ Could not compact the history: {error}',
|
|
66
|
+
},
|
|
67
|
+
'ds.context_limit_give_up': {
|
|
68
|
+
ru: '✖ {provider}: лимит длины чата исчерпан, и сжать историю не удалось — этот чат больше не принимает сообщения. Открой новый чат (/new) и повтори задачу.',
|
|
69
|
+
en: '✖ {provider}: the chat length limit is reached and the history could not be compacted — this chat no longer accepts messages. Open a new chat (/new) and repeat the task.',
|
|
70
|
+
},
|
|
59
71
|
'ds.input_missing': {
|
|
60
72
|
ru: '✖ Не найдено поле ввода на странице {provider}. Запусти /debug-dom и поправь INPUT_SELECTORS.',
|
|
61
73
|
en: '✖ The {provider} input field was not found. Run /debug-dom and fix INPUT_SELECTORS.',
|
package/dist/i18n/glm.js
CHANGED
|
@@ -43,6 +43,18 @@ export const glmMessages = {
|
|
|
43
43
|
ru: '⚠ Отправка не удалась (попытка {attempt}/{max} на этот запрос): {error} — повторяю...',
|
|
44
44
|
en: '⚠ Send failed (attempt {attempt}/{max} for this request): {error} — retrying...',
|
|
45
45
|
},
|
|
46
|
+
'glm.context_limit_compact': {
|
|
47
|
+
ru: '🗜️ {provider}: лимит длины чата исчерпан — сжимаю историю и продолжаю в новом чате...',
|
|
48
|
+
en: '🗜️ {provider}: the chat hit its context limit — compacting the history and continuing in a fresh chat...',
|
|
49
|
+
},
|
|
50
|
+
'glm.context_limit_compact_failed': {
|
|
51
|
+
ru: '⚠ Не удалось сжать историю: {error}',
|
|
52
|
+
en: '⚠ Could not compact the history: {error}',
|
|
53
|
+
},
|
|
54
|
+
'glm.context_limit_give_up': {
|
|
55
|
+
ru: '✖ {provider}: лимит длины чата исчерпан, и сжать историю не удалось. Открой новый чат (/new) и повтори задачу.',
|
|
56
|
+
en: '✖ {provider}: the chat context limit is reached and the history could not be compacted. Open a new chat (/new) and repeat the task.',
|
|
57
|
+
},
|
|
46
58
|
'glm.ask_failed': {
|
|
47
59
|
ru: '✖ Не удалось получить ответ от {provider} после {max} попыток: {error}',
|
|
48
60
|
en: '✖ Could not get an answer from {provider} after {max} attempts: {error}',
|
package/dist/i18n/qwen.js
CHANGED
|
@@ -145,6 +145,18 @@ export const qwenMessages = {
|
|
|
145
145
|
ru: '⚠ Отправка не удалась (попытка {attempt}/{max} на этот запрос): {error} — повторяю...',
|
|
146
146
|
en: '⚠ Send failed (attempt {attempt}/{max} for this request): {error} — retrying...',
|
|
147
147
|
},
|
|
148
|
+
'qwen.context_limit_compact': {
|
|
149
|
+
ru: '🗜️ {provider}: лимит длины чата исчерпан — сжимаю историю и продолжаю в новом чате...',
|
|
150
|
+
en: '🗜️ {provider}: the chat hit its context limit — compacting the history and continuing in a fresh chat...',
|
|
151
|
+
},
|
|
152
|
+
'qwen.context_limit_compact_failed': {
|
|
153
|
+
ru: '⚠ Не удалось сжать историю: {error}',
|
|
154
|
+
en: '⚠ Could not compact the history: {error}',
|
|
155
|
+
},
|
|
156
|
+
'qwen.context_limit_give_up': {
|
|
157
|
+
ru: '✖ {provider}: лимит длины чата исчерпан, и сжать историю не удалось. Открой новый чат (/new) и повтори задачу.',
|
|
158
|
+
en: '✖ {provider}: the chat context limit is reached and the history could not be compacted. Open a new chat (/new) and repeat the task.',
|
|
159
|
+
},
|
|
148
160
|
'qwen.ask_failed': {
|
|
149
161
|
ru: '✖ Не удалось получить ответ от {provider} после {max} попыток: {error}',
|
|
150
162
|
en: '✖ Could not get an answer from {provider} after {max} attempts: {error}',
|
package/dist/index.js
CHANGED
|
@@ -797,7 +797,7 @@ function applyModelContextLimit(model) {
|
|
|
797
797
|
* the choice is a class, not a flag; the shared timings live in
|
|
798
798
|
* config.browser and only the provider's own section is overridden.
|
|
799
799
|
*/
|
|
800
|
-
function createProviderBrowser(provider) {
|
|
800
|
+
function createProviderBrowser(provider, onContextLimit = null) {
|
|
801
801
|
// Saved credentials go into the ACTIVE provider's section: browser.auth
|
|
802
802
|
// belongs to DeepSeek (so old configs keep working), qwen.auth / glm.auth
|
|
803
803
|
// are their own. Sharing one pair meant a DeepSeek login was typed into the
|
|
@@ -840,6 +840,7 @@ function createProviderBrowser(provider) {
|
|
|
840
840
|
loginTimeoutMs: config.browser.loginTimeoutMs,
|
|
841
841
|
locale: currentLocale,
|
|
842
842
|
onAuthSave: persistAuth,
|
|
843
|
+
onContextLimit,
|
|
843
844
|
});
|
|
844
845
|
case 'glm':
|
|
845
846
|
return new GlmBrowser({
|
|
@@ -860,6 +861,7 @@ function createProviderBrowser(provider) {
|
|
|
860
861
|
loginTimeoutMs: config.browser.loginTimeoutMs,
|
|
861
862
|
locale: currentLocale,
|
|
862
863
|
onAuthSave: persistAuth,
|
|
864
|
+
onContextLimit,
|
|
863
865
|
});
|
|
864
866
|
case 'deepseek':
|
|
865
867
|
default:
|
|
@@ -870,6 +872,7 @@ function createProviderBrowser(provider) {
|
|
|
870
872
|
...config.browser,
|
|
871
873
|
locale: currentLocale,
|
|
872
874
|
onAuthSave: persistAuth,
|
|
875
|
+
onContextLimit,
|
|
873
876
|
});
|
|
874
877
|
}
|
|
875
878
|
}
|
|
@@ -1061,7 +1064,15 @@ async function main() {
|
|
|
1061
1064
|
// `let`, not `const`: a live /provider switch disposes this instance and
|
|
1062
1065
|
// builds the other one IN PLACE (see the /provider block in the main loop),
|
|
1063
1066
|
// so the URL changes without restarting zames.
|
|
1064
|
-
|
|
1067
|
+
// The context-limit recovery is a closure over main() session state, but
|
|
1068
|
+
// the browser is built BEFORE that state exists and it needs the callback
|
|
1069
|
+
// at construction. This holder breaks the cycle: the factory reads it
|
|
1070
|
+
// lazily on every recovery, and it is filled right after the session
|
|
1071
|
+
// variables are set up (before the first task). Null = the provider fails
|
|
1072
|
+
// with a clear message instead of retrying into a dead chat.
|
|
1073
|
+
let contextLimitRecovery = null;
|
|
1074
|
+
const onContextLimit = () => contextLimitRecovery ? contextLimitRecovery() : Promise.resolve(false);
|
|
1075
|
+
let browser = createProviderBrowser(asProviderId(config.provider), onContextLimit);
|
|
1065
1076
|
const bootSpinner = mod.createSpinner(currentLocale);
|
|
1066
1077
|
bootSpinner.thinking();
|
|
1067
1078
|
try {
|
|
@@ -1188,6 +1199,44 @@ async function main() {
|
|
|
1188
1199
|
}
|
|
1189
1200
|
let freshChatNext = true;
|
|
1190
1201
|
let sendSystemPromptNext = true;
|
|
1202
|
+
/**
|
|
1203
|
+
* Recover from "Length limit reached. Please start a new chat.": the chat
|
|
1204
|
+
* refuses EVERY further send, so the only way to keep working is compaction
|
|
1205
|
+
* into a fresh chat. Returns true when a new chat holds the system prompt
|
|
1206
|
+
* and the handover, i.e. the browser may resend the refused message there.
|
|
1207
|
+
*
|
|
1208
|
+
* `contextExhausted` skips the summary call inside performCompact: asking the
|
|
1209
|
+
* OLD chat to summarize itself is refused exactly like the send that failed,
|
|
1210
|
+
* so the summary is built from the history endpoint instead.
|
|
1211
|
+
*/
|
|
1212
|
+
const recoverFromContextLimit = async () => {
|
|
1213
|
+
const res = await performCompact({
|
|
1214
|
+
browser,
|
|
1215
|
+
currentChatId,
|
|
1216
|
+
workdir: currentWorkdir,
|
|
1217
|
+
locale: currentLocale,
|
|
1218
|
+
transcript,
|
|
1219
|
+
ui: editor,
|
|
1220
|
+
buildSystemPrompt: mod.buildSystemPrompt,
|
|
1221
|
+
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
1222
|
+
answerTimeoutMs: providerAnswerTimeoutMs(),
|
|
1223
|
+
quiet: true,
|
|
1224
|
+
skipUiLock: true,
|
|
1225
|
+
selfImprovement: devMode,
|
|
1226
|
+
contextExhausted: true,
|
|
1227
|
+
});
|
|
1228
|
+
if (!res.ok)
|
|
1229
|
+
return false;
|
|
1230
|
+
currentChatId = res.chatId;
|
|
1231
|
+
if (currentChatId)
|
|
1232
|
+
saveLastChat(currentChatId, currentWorkdir);
|
|
1233
|
+
// The fresh chat already carries the system prompt and the handover, and
|
|
1234
|
+
// the session continues in it — later tasks must not open yet another chat.
|
|
1235
|
+
freshChatNext = false;
|
|
1236
|
+
sendSystemPromptNext = false;
|
|
1237
|
+
return true;
|
|
1238
|
+
};
|
|
1239
|
+
contextLimitRecovery = recoverFromContextLimit;
|
|
1191
1240
|
// Resuming an existing chat already has the system-prompt at its start, so
|
|
1192
1241
|
// resending it is wasteful and pollutes the context. Default: do NOT resend.
|
|
1193
1242
|
// `--resend-prompt` or `browser.resendPromptOnResume` forces it (e.g. when
|
|
@@ -2688,7 +2737,7 @@ async function main() {
|
|
|
2688
2737
|
// If we closed the old browser before the new one launched and the
|
|
2689
2738
|
// launch failed, `browser` would point at a dead instance and every
|
|
2690
2739
|
// later command would break.
|
|
2691
|
-
const nextBrowser = createProviderBrowser(arg);
|
|
2740
|
+
const nextBrowser = createProviderBrowser(arg, onContextLimit);
|
|
2692
2741
|
// Wire the Ink login BEFORE waitForLogin(), or the new backend would
|
|
2693
2742
|
// fall back to readline and pause the TUI for its sign-in.
|
|
2694
2743
|
wireLoginCallbacks(nextBrowser);
|
|
@@ -4144,6 +4193,7 @@ async function main() {
|
|
|
4144
4193
|
return res.ok ? res.chatId : null;
|
|
4145
4194
|
}
|
|
4146
4195
|
: null,
|
|
4196
|
+
onContextLimit: recoverFromContextLimit,
|
|
4147
4197
|
autoCompactPct: config.browser.autoCompactPct,
|
|
4148
4198
|
contextLimit: effectiveContextLimit(),
|
|
4149
4199
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
package/dist/qwen-browser.js
CHANGED
|
@@ -24,12 +24,13 @@ import { rewriteFailedAttachments } from './commands.js';
|
|
|
24
24
|
import { interactiveBrowserLogin, loginIsInteractive, canShowWindow, } from './browser-login.js';
|
|
25
25
|
import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
|
|
26
26
|
import { theme } from './theme.js';
|
|
27
|
+
import { recoverFromContextLimit } from './context-limit.js';
|
|
27
28
|
import { scrapeQwenChatList, scrapeQwenChatMessages, parseQwenChatList, parseQwenHistory, } from './qwen-chats.js';
|
|
28
29
|
import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
|
|
29
30
|
import { chatSignalInDom, dumpDomCountsInDom, isElementEnabledInDom, readInputTrimmedTextInDom, readLastAnswerInDom, readPageToastsInDom, readUserAgentInDom, setBusyTitleInDom, clickStopInDom, stopButtonVisibleInDom, } from './browser-dom.js';
|
|
30
31
|
import { probeUiHealth, } from './ui-health.js';
|
|
31
32
|
import { CHAT_URL, CHAT_ID_RE, chatUrl, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, NEW_CHAT_SELECTOR, MODEL_TRIGGER_SELECTOR, MODELS_URL, CHAT_LIST_URL, chatHistoryUrl, AGE_GATE_MODAL, AGE_GATE_CONTINUE, AGE_GATE_YEAR_SELECT, AGE_GATE_YEAR_OPTION, COOKIE_ACCEPT_RE, ATTACH_SELECTORS, ATTACH_MENU_TRIGGER, ATTACH_MENU_ITEM_RE, THINKING_SELECTOR, THINKING_MODE_RE, FAST_MODE_RE, WEB_SEARCH_MENU_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, ASSISTANT_CLASS_RE, CHAT_SIGNAL_NODES_SELECTOR, MARKDOWN_SELECTOR, TOGGLE_SELECTORS, } from './qwen-ui.js';
|
|
32
|
-
import { extractAnswer, extractTokenUsage, isFinishedWithoutAnswer, isRateLimitText, isGuestLimitText, isServerBusyText, } from './qwen-net-capture.js';
|
|
33
|
+
import { extractAnswer, extractTokenUsage, isFinishedWithoutAnswer, isRateLimitText, isGuestLimitText, isContextLimitText, isServerBusyText, } from './qwen-net-capture.js';
|
|
33
34
|
// A SEPARATE profile from DeepSeek's ~/.zames/profile. Two persistent Chromium
|
|
34
35
|
// contexts on ONE --user-data-dir cannot coexist (SingletonLock), and keeping
|
|
35
36
|
// Qwen's cookies apart also means switching providers never logs the other
|
|
@@ -104,6 +105,10 @@ export class QwenBrowser {
|
|
|
104
105
|
// body is the reliable signal. ask() then backs off instead of waiting out
|
|
105
106
|
// the whole answer deadline.
|
|
106
107
|
_netServerBusy;
|
|
108
|
+
// True when the chat's CONTEXT WINDOW is exhausted. The chat refuses every
|
|
109
|
+
// further send, so this is NOT retryable in place: the caller compacts into
|
|
110
|
+
// a fresh chat (onContextLimit) and ask() resends there.
|
|
111
|
+
_netContextLimit;
|
|
107
112
|
_netChatId;
|
|
108
113
|
_lastTokenUsage;
|
|
109
114
|
_toggles;
|
|
@@ -126,7 +131,12 @@ export class QwenBrowser {
|
|
|
126
131
|
loginTimeoutMs;
|
|
127
132
|
_windowClosed;
|
|
128
133
|
_carryCookies;
|
|
129
|
-
|
|
134
|
+
// Ask() budget for the context-limit recovery. A compaction that frees
|
|
135
|
+
// nothing leaves the chat just as rejected, so this is independent of
|
|
136
|
+
// askRetries.
|
|
137
|
+
maxContextLimitRetries;
|
|
138
|
+
onContextLimit;
|
|
139
|
+
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, maxContextLimitRetries = 1, onContextLimit, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, loginTimeoutMs = 600000, userAgent, } = {}) {
|
|
130
140
|
this.headless = headless;
|
|
131
141
|
this.debug = debug;
|
|
132
142
|
this.channel = channel;
|
|
@@ -141,6 +151,8 @@ export class QwenBrowser {
|
|
|
141
151
|
this.serverBusyWaitMs = serverBusyWaitMs;
|
|
142
152
|
this.maxIncompleteRetries = maxIncompleteRetries;
|
|
143
153
|
this.incompleteWaitMs = incompleteWaitMs;
|
|
154
|
+
this.maxContextLimitRetries = maxContextLimitRetries;
|
|
155
|
+
this.onContextLimit = onContextLimit || null;
|
|
144
156
|
this.startWaitMs = startWaitMs;
|
|
145
157
|
this.sendButtonWaitMs = sendButtonWaitMs;
|
|
146
158
|
this.model = model;
|
|
@@ -167,6 +179,7 @@ export class QwenBrowser {
|
|
|
167
179
|
this._netRateLimited = false;
|
|
168
180
|
this._netGuestLimit = false;
|
|
169
181
|
this._netServerBusy = false;
|
|
182
|
+
this._netContextLimit = false;
|
|
170
183
|
this._netChatId = null;
|
|
171
184
|
this._lastTokenUsage = null;
|
|
172
185
|
this._toggles = { deepThinking, webSearch };
|
|
@@ -455,6 +468,10 @@ export class QwenBrowser {
|
|
|
455
468
|
this._netNoAnswer = true;
|
|
456
469
|
this._netCapture = '';
|
|
457
470
|
}
|
|
471
|
+
if (isContextLimitText(body)) {
|
|
472
|
+
this._netContextLimit = true;
|
|
473
|
+
this._netCapture = '';
|
|
474
|
+
}
|
|
458
475
|
if (isRateLimitText(body)) {
|
|
459
476
|
this._netRateLimited = true;
|
|
460
477
|
this._netCapture = '';
|
|
@@ -1604,6 +1621,9 @@ export class QwenBrowser {
|
|
|
1604
1621
|
let rateLimitRetries = 0;
|
|
1605
1622
|
let serverBusyRetries = 0;
|
|
1606
1623
|
let incompleteRetries = 0;
|
|
1624
|
+
// The exhausted-context recovery is a COMPACTION (a new chat), not a
|
|
1625
|
+
// resend, so it has its own budget.
|
|
1626
|
+
let contextLimitRetries = 0;
|
|
1607
1627
|
this._askInFlight++;
|
|
1608
1628
|
try {
|
|
1609
1629
|
while (attempt < this.askRetries) {
|
|
@@ -1631,6 +1651,32 @@ export class QwenBrowser {
|
|
|
1631
1651
|
// clear "sign in" message instead of burning the retry budget.
|
|
1632
1652
|
throw new Error(this._t('qwen.age_gate_block'));
|
|
1633
1653
|
}
|
|
1654
|
+
// The chat ran out of context: it refuses every further send, so
|
|
1655
|
+
// resending in place can never succeed. The injected callback
|
|
1656
|
+
// compacts the chat into a fresh one and the SAME prompt is
|
|
1657
|
+
// resent there.
|
|
1658
|
+
if (e instanceof QwenContextLimitError) {
|
|
1659
|
+
contextLimitRetries++;
|
|
1660
|
+
const res = await recoverFromContextLimit({
|
|
1661
|
+
recover: () => this.onContextLimit?.() ?? Promise.resolve(false),
|
|
1662
|
+
maxRetries: this.maxContextLimitRetries,
|
|
1663
|
+
attempt: contextLimitRetries,
|
|
1664
|
+
notice: (text) => this._notice(text),
|
|
1665
|
+
t: (k, p) => this._t(k, p),
|
|
1666
|
+
keys: {
|
|
1667
|
+
compact: 'qwen.context_limit_compact',
|
|
1668
|
+
compactFailed: 'qwen.context_limit_compact_failed',
|
|
1669
|
+
giveUp: 'qwen.context_limit_give_up',
|
|
1670
|
+
},
|
|
1671
|
+
provider: 'Qwen',
|
|
1672
|
+
}, e);
|
|
1673
|
+
if (!res.resend)
|
|
1674
|
+
throw res.error || e;
|
|
1675
|
+
// The compaction already typed into the new chat, so the send
|
|
1676
|
+
// throttle must count from that moment.
|
|
1677
|
+
this._lastSentAt = Date.now();
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1634
1680
|
if (/rate limit|too frequent/i.test(msg)) {
|
|
1635
1681
|
if (rateLimitRetries >= this.maxRateLimitRetries)
|
|
1636
1682
|
break;
|
|
@@ -1830,6 +1876,7 @@ export class QwenBrowser {
|
|
|
1830
1876
|
this._netRateLimited = false;
|
|
1831
1877
|
this._netGuestLimit = false;
|
|
1832
1878
|
this._netServerBusy = false;
|
|
1879
|
+
this._netContextLimit = false;
|
|
1833
1880
|
this._netBody = '';
|
|
1834
1881
|
await this._applyToggles();
|
|
1835
1882
|
if (attachments.length) {
|
|
@@ -1937,6 +1984,9 @@ export class QwenBrowser {
|
|
|
1937
1984
|
if (isRateLimitText(pageText)) {
|
|
1938
1985
|
throw new QwenRateLimitError(pageText.slice(0, 300));
|
|
1939
1986
|
}
|
|
1987
|
+
if (this._netContextLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
1988
|
+
throw new QwenContextLimitError(this._netBody.slice(0, 300));
|
|
1989
|
+
}
|
|
1940
1990
|
if (this._netGuestLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
1941
1991
|
throw new QwenGuestLimitError(this._netBody.slice(0, 200));
|
|
1942
1992
|
}
|
|
@@ -1971,6 +2021,9 @@ export class QwenBrowser {
|
|
|
1971
2021
|
if (isRateLimitText(pageText)) {
|
|
1972
2022
|
throw new QwenRateLimitError(pageText.slice(0, 300));
|
|
1973
2023
|
}
|
|
2024
|
+
if (this._netContextLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
2025
|
+
throw new QwenContextLimitError(this._netBody.slice(0, 300));
|
|
2026
|
+
}
|
|
1974
2027
|
if (this._netGuestLimit && this._netCaptureAt >= this._lastSentAt) {
|
|
1975
2028
|
throw new QwenGuestLimitError(this._netBody.slice(0, 200));
|
|
1976
2029
|
}
|
|
@@ -2033,6 +2086,14 @@ class QwenAgeGateError extends Error {
|
|
|
2033
2086
|
this.name = 'QwenAgeGateError';
|
|
2034
2087
|
}
|
|
2035
2088
|
}
|
|
2089
|
+
// The chat hit its CONTEXT LIMIT and refuses the turn. NOT retryable in
|
|
2090
|
+
// place: ask() compacts into a fresh chat and resends the prompt there.
|
|
2091
|
+
class QwenContextLimitError extends Error {
|
|
2092
|
+
constructor(detail) {
|
|
2093
|
+
super('context limit: ' + detail);
|
|
2094
|
+
this.name = 'QwenContextLimitError';
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2036
2097
|
class QwenIncompleteError extends Error {
|
|
2037
2098
|
constructor(detail) {
|
|
2038
2099
|
super('generation incomplete: ' + detail);
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -283,3 +283,21 @@ export function isServerBusyText(body) {
|
|
|
283
283
|
return false;
|
|
284
284
|
return /server is busy|service unavailable|try again later/i.test(b);
|
|
285
285
|
}
|
|
286
|
+
// The chat's CONTEXT WINDOW is exhausted. Qwen refuses the turn and no retry
|
|
287
|
+
// in the same chat can succeed - the only way out is a compaction into a
|
|
288
|
+
// fresh chat. Same guard as the other classifiers: the notice must be an
|
|
289
|
+
// error payload or a body with NO answer content, because the model can
|
|
290
|
+
// legitimately WRITE about context limits and treating that answer as a
|
|
291
|
+
// refusal would lose a good turn.
|
|
292
|
+
export function isContextLimitText(body) {
|
|
293
|
+
const b = String(body || '');
|
|
294
|
+
if (!b)
|
|
295
|
+
return false;
|
|
296
|
+
const explicit = /context (?:length|window) (?:is )?(?:exceeded|too long|limit)|maximum context length|exceeds? the (?:maximum |model'?s )?context|too many (?:input )?tokens|length limit (?:has been )?reached|start a new (?:chat|conversation)|лимит длин|превышен[ао]? (?:максимальн[аяу]? )?(?:длина|контекст)|начните новый чат/i.test(b);
|
|
297
|
+
if (!explicit)
|
|
298
|
+
return false;
|
|
299
|
+
const errNotice = /event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b);
|
|
300
|
+
if (!errNotice && hasAnswerContent(b))
|
|
301
|
+
return false;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
package/dist/run-task.js
CHANGED
|
@@ -284,7 +284,7 @@ async function evaluateGoal(browser, condition, config, transcript) {
|
|
|
284
284
|
}
|
|
285
285
|
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
286
286
|
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
287
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
|
|
287
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, onContextLimit = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
|
|
288
288
|
// The ACTIVE goal for the duration of this run. It is mutable because a
|
|
289
289
|
// completion goal can be cleared mid-run (verdict MET/IMPOSSIBLE, or the
|
|
290
290
|
// operator runs `/goal clear` while the task is in flight). `goalQuery`
|
|
@@ -411,6 +411,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
411
411
|
// outer currentChatId; it serializes with the throttle because the
|
|
412
412
|
// loop awaits it.
|
|
413
413
|
onAutoCompact,
|
|
414
|
+
onContextLimit,
|
|
414
415
|
autoCompactPct,
|
|
415
416
|
contextLimit,
|
|
416
417
|
getTokenUsage,
|
package/dist/shell.js
CHANGED
|
@@ -105,6 +105,16 @@ export function runShell(workdir, command, timeout = 30_000, signal) {
|
|
|
105
105
|
const safeTimeout = typeof timeout === 'number' && Number.isFinite(timeout) && timeout >= 0
|
|
106
106
|
? Math.floor(timeout)
|
|
107
107
|
: 30_000;
|
|
108
|
+
// An EMPTY command is rejected by exec SYNCHRONOUSLY
|
|
109
|
+
// (`ERR_INVALID_ARG_VALUE: The argument 'file' cannot be empty`), so the
|
|
110
|
+
// throw escaped the promise below and the caller crashed instead of
|
|
111
|
+
// reporting the problem: the Bash tool after req() accepted a
|
|
112
|
+
// whitespace-only command, and the operator's `!` escape never passed
|
|
113
|
+
// through req() at all. Answer with the same wording an empty successful
|
|
114
|
+
// run uses, so every caller just prints it.
|
|
115
|
+
if (!String(command ?? '').trim()) {
|
|
116
|
+
return Promise.resolve('(command produced no output)');
|
|
117
|
+
}
|
|
108
118
|
return new Promise((resolve) => {
|
|
109
119
|
const options = {
|
|
110
120
|
cwd: workdir,
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
import { useSyncExternalStore } from 'react';
|
|
13
13
|
import { Box, Text, useWindowSize, useInput } from 'ink';
|
|
14
14
|
import { LogList } from './LogView.js';
|
|
15
|
-
import { windowByRows, totalRows } from './log-window.js';
|
|
15
|
+
import { windowByRows, totalRows, entryRows } from './log-window.js';
|
|
16
16
|
import { StatusBar } from './StatusBar.js';
|
|
17
17
|
import { InputLine } from './InputLine.js';
|
|
18
18
|
import { contentWidth } from '../width.js';
|
|
@@ -75,10 +75,22 @@ export function FullScreenApp({ store, inputProps, }) {
|
|
|
75
75
|
// it half empty. `scroll` is measured in ROWS here (0 = newest at the
|
|
76
76
|
// bottom).
|
|
77
77
|
const all = state.log;
|
|
78
|
+
// The viewport height is RESERVED with the floor (never more rows than the
|
|
79
|
+
// pane can hold) and the box is allowed to take the slack: rounding the
|
|
80
|
+
// reservation UP on every entry made the panel taller than the terminal on a
|
|
81
|
+
// short window, so the input rows fell below the last screen row and the
|
|
82
|
+
// caret drifted under the box while a long multi-line buffer was typed.
|
|
83
|
+
const totalFloor = totalRows(all, cols, 'floor');
|
|
78
84
|
const total = totalRows(all, cols);
|
|
79
85
|
const maxScroll = Math.max(0, total - logHeight);
|
|
80
86
|
const scroll = Math.max(0, Math.min(state.scroll, maxScroll));
|
|
81
87
|
const visible = windowByRows(all, logHeight, scroll, cols);
|
|
88
|
+
// Rows the WINDOWED content really needs. The reservation above rounds DOWN
|
|
89
|
+
// per entry (floor), so the difference is the slack the content needs. The
|
|
90
|
+
// box takes at most `logHeight` and at least one row; never more, because
|
|
91
|
+
// that is exactly what pushed the input rows off a short screen.
|
|
92
|
+
const filledRows = visible.reduce((n, e) => n + entryRows(e, cols), 0);
|
|
93
|
+
const logBoxHeight = Math.max(1, Math.min(logHeight, Math.max(filledRows, 1)));
|
|
82
94
|
// Scroll the log with PgUp/PgDn; Home/End jump to the very top/bottom. The
|
|
83
95
|
// input line OWNS Home/End while its buffer is non-empty (they move the
|
|
84
96
|
// caret), so the jump only applies to an EMPTY buffer — which is exactly
|
|
@@ -111,9 +123,17 @@ export function FullScreenApp({ store, inputProps, }) {
|
|
|
111
123
|
const next = cur + delta * 3;
|
|
112
124
|
store.set({ scroll: Math.max(0, Math.min(maxScroll, next)) });
|
|
113
125
|
}, state.mouseScroll);
|
|
114
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column",
|
|
126
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 1, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: "#5b616e", children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
|
|
115
127
|
// Cursor rows above the input box: header + log viewport + status.
|
|
116
|
-
|
|
128
|
+
// The log pane is FLEXIBLE now, so the panel below it starts right
|
|
129
|
+
// after everything above: header + as many log rows as fit + the
|
|
130
|
+
// suggestion popup + status. `cursorTop` counts the rows ABOVE the
|
|
131
|
+
// input rows; the box border row is added by InputLine.
|
|
132
|
+
cursorTop: HEADER_ROWS +
|
|
133
|
+
Math.max(1, termRows - panelRows) +
|
|
134
|
+
suggestRows +
|
|
135
|
+
STATUS_ROWS +
|
|
136
|
+
1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: "#5b616e", wrap: "truncate", children: ' ' +
|
|
117
137
|
translate(state.locale)(state.mouseScroll ? 'editor.keys_hint' : 'editor.keys_hint_select') })] }));
|
|
118
138
|
}
|
|
119
139
|
void contentWidth;
|
package/dist/tui/log-window.js
CHANGED
|
@@ -41,21 +41,30 @@ function visWidth(s) {
|
|
|
41
41
|
* How many terminal rows one log entry occupies at `cols` columns. A line
|
|
42
42
|
* longer than the width wraps (Ink uses hard wrapping for these rows). An
|
|
43
43
|
* empty line still takes one row.
|
|
44
|
+
*
|
|
45
|
+
* `floor` gives the UNDER-estimate used to RESERVE the log viewport height;
|
|
46
|
+
* the exact `ceil` is what the content really renders, so the two together let
|
|
47
|
+
* the log box take the slack (see FullScreenApp) without ever overflowing —
|
|
48
|
+
* an overflow is what pushed the input rows below the terminal and left the
|
|
49
|
+
* caret drifting under the box on a short terminal.
|
|
44
50
|
*/
|
|
45
|
-
export function entryRows(entry, cols) {
|
|
51
|
+
export function entryRows(entry, cols, mode = 'exact') {
|
|
46
52
|
const width = Math.max(1, cols);
|
|
47
53
|
const lines = String(entry.text ?? '').split(String.fromCharCode(10));
|
|
48
54
|
let rows = 0;
|
|
49
55
|
for (const line of lines) {
|
|
50
|
-
|
|
56
|
+
const w = visWidth(line);
|
|
57
|
+
const n = mode === 'floor' ? Math.floor(w / width) : Math.ceil(w / width);
|
|
58
|
+
rows += Math.max(1, n);
|
|
51
59
|
}
|
|
52
60
|
return Math.max(1, rows);
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Slice the log so its RENDERED height fits `height` rows, keeping `scroll`
|
|
56
64
|
* rows hidden from the bottom (0 = pinned to the newest). Returns the visible
|
|
57
|
-
* entries. A single entry taller than the viewport is kept whole (its tail
|
|
58
|
-
* shown) rather than dropped
|
|
65
|
+
* entries. A single entry taller than the viewport is kept whole (its tail
|
|
66
|
+
* is shown) rather than dropped — the pane is overflow="hidden" with
|
|
67
|
+
* justifyContent="flex-end", so the TAIL is what stays on screen.
|
|
59
68
|
*/
|
|
60
69
|
export function windowByRows(entries, height, scroll, cols) {
|
|
61
70
|
const cap = Math.max(1, Math.floor(height));
|
|
@@ -85,9 +94,9 @@ export function windowByRows(entries, height, scroll, cols) {
|
|
|
85
94
|
return picked;
|
|
86
95
|
}
|
|
87
96
|
/** Total rendered rows of a list of entries (for maxScroll). */
|
|
88
|
-
export function totalRows(entries, cols) {
|
|
97
|
+
export function totalRows(entries, cols, mode = 'exact') {
|
|
89
98
|
let n = 0;
|
|
90
99
|
for (const e of entries)
|
|
91
|
-
n += entryRows(e, cols);
|
|
100
|
+
n += entryRows(e, cols, mode);
|
|
92
101
|
return n;
|
|
93
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.82.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": {
|