zames_pro 2.90.18 → 2.92.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/CHANGELOG.md +17 -0
- package/dist/agent-loop.js +18 -5
- package/dist/ask-retry.js +9 -48
- package/dist/browser-login.js +8 -2
- package/dist/browser.js +44 -17
- package/dist/config-live.js +8 -0
- package/dist/config-schema.js +16 -0
- package/dist/config.js +7 -0
- package/dist/glm-browser.js +35 -17
- package/dist/i18n/config.js +8 -0
- package/dist/index.js +2 -0
- package/dist/qwen-browser.js +72 -32
- package/dist/restore-history.js +25 -0
- package/dist/retry-backoff.js +89 -0
- package/dist/retry-policy.js +98 -0
- package/dist/run-task.js +2 -0
- package/dist/subagent.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.92.0] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Internal improvements.
|
|
15
|
+
|
|
16
|
+
## [2.91.0] - 2026-11-01
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **retry:** infinite incremental retries with escalating backoff
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **qwen,glm:** headless login probe and headed runs
|
|
25
|
+
- **ui:** same no-flicker phase rule for qwen and glm
|
|
26
|
+
|
|
10
27
|
## [2.90.18] - 2026-10-30
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/dist/agent-loop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { backoffWaitMs } from './retry-backoff.js';
|
|
1
2
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
2
3
|
import { isContextLimitText } from './browser-pure.js';
|
|
3
4
|
import { providerLabel as providerHumanLabel } from './providers.js';
|
|
@@ -12,7 +13,7 @@ import { shouldRunDiagnostics, runDiagnostics, } from './diagnostics.js';
|
|
|
12
13
|
// The tools that WRITE/EDIT a file (and take a `path`). The completion nudge
|
|
13
14
|
// only cares about these — `Bash`/`Git*` in MUTATING_TOOLS are not edits.
|
|
14
15
|
const EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'ApplyPatch']);
|
|
15
|
-
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 = DEFAULT_LOCALE, askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
16
|
+
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 = DEFAULT_LOCALE, askDeadlineMs = 240_000, maxAfterToolRetries = 6, infiniteRetries = true, maxRetryWaitMs = 30 * 60_000, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
16
17
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
17
18
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
18
19
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
@@ -292,6 +293,15 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
292
293
|
// The nudge is sent at most ONCE per task: it is a hint, not a guard.
|
|
293
294
|
let completionNudgeSent = false;
|
|
294
295
|
const MAX_AFTER_TOOL_RETRIES = Math.max(0, Math.floor(maxAfterToolRetries));
|
|
296
|
+
// The watchdog and the transient send families share ONE backoff
|
|
297
|
+
// (src/retry-backoff.ts): with infiniteRetries on (default) the wait grows
|
|
298
|
+
// 5s -> 10s -> ... up to maxRetryWaitMs instead of giving up.
|
|
299
|
+
const watchdogBackoff = {
|
|
300
|
+
infiniteRetries,
|
|
301
|
+
maxRetries: MAX_AFTER_TOOL_RETRIES,
|
|
302
|
+
baseWaitMs: 5000,
|
|
303
|
+
maxWaitMs: maxRetryWaitMs,
|
|
304
|
+
};
|
|
295
305
|
// One-time hint when the context nears full while auto-compact is OFF:
|
|
296
306
|
// the operator is told about /compact instead of silently degrading. Set
|
|
297
307
|
// once per task so the terminal is not spammed.
|
|
@@ -384,10 +394,13 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
384
394
|
// single hiccup from a genuine stall (the budget used to be invisible).
|
|
385
395
|
safeWarning(tr('ds.answer_timeout', {
|
|
386
396
|
sec: Math.round(askDeadlineMs / 1000),
|
|
387
|
-
attempt:
|
|
388
|
-
|
|
397
|
+
attempt: infiniteRetries
|
|
398
|
+
? afterToolRetries + 1
|
|
399
|
+
: Math.min(afterToolRetries + 1, MAX_AFTER_TOOL_RETRIES),
|
|
400
|
+
max: infiniteRetries ? '∞' : MAX_AFTER_TOOL_RETRIES,
|
|
389
401
|
}));
|
|
390
|
-
|
|
402
|
+
const watchdogWaitMs = backoffWaitMs(watchdogBackoff, afterToolRetries + 1);
|
|
403
|
+
if (infiniteRetries || afterToolRetries < MAX_AFTER_TOOL_RETRIES) {
|
|
391
404
|
afterToolRetries++;
|
|
392
405
|
// The retry used to `continue` with the UNCHANGED `message`: a bare
|
|
393
406
|
// re-send of the same task text. The model then saw the task arrive a
|
|
@@ -401,7 +414,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
401
414
|
'no text before or after, for example: ' +
|
|
402
415
|
'{"tool": "Bash", "args": {"command": "..."}}. ' +
|
|
403
416
|
'If the task is really done — call respond with the final message.';
|
|
404
|
-
await new Promise((r) => setTimeout(r,
|
|
417
|
+
await new Promise((r) => setTimeout(r, watchdogWaitMs));
|
|
405
418
|
continue;
|
|
406
419
|
}
|
|
407
420
|
transcript?.log('ask_timeout_exhausted', {
|
package/dist/ask-retry.js
CHANGED
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
// N1 stage 4: the retry POLICY of a provider's
|
|
1
|
+
// N1 stage 4: the retry POLICY of a provider's ask() as a PURE function.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
* Decide what to do after a failed attempt.
|
|
12
|
-
*
|
|
13
|
-
* `classify` maps the thrown error to a family; it lives in the provider
|
|
14
|
-
* because the backend-specific markers (guest limit, age gate, captcha) are
|
|
15
|
-
* provider knowledge. A `null` classification falls through to the generic
|
|
16
|
-
* attempt budget.
|
|
17
|
-
*/
|
|
18
|
-
export function decideRetry(family, state, budgets, waits) {
|
|
19
|
-
switch (family) {
|
|
20
|
-
case 'fatal':
|
|
21
|
-
return { kind: 'give-up', family };
|
|
22
|
-
case 'context-limit':
|
|
23
|
-
if (state.contextLimitRetries >= budgets.maxContextLimitRetries) {
|
|
24
|
-
return { kind: 'give-up', family };
|
|
25
|
-
}
|
|
26
|
-
return { kind: 'compact', family };
|
|
27
|
-
case 'rate-limit':
|
|
28
|
-
if (state.rateLimitRetries >= budgets.maxRateLimitRetries) {
|
|
29
|
-
return { kind: 'give-up', family };
|
|
30
|
-
}
|
|
31
|
-
return { kind: 'wait', family, waitMs: waits.rateLimitMs };
|
|
32
|
-
case 'server-busy':
|
|
33
|
-
if (state.serverBusyRetries >= budgets.maxServerBusyRetries) {
|
|
34
|
-
return { kind: 'give-up', family };
|
|
35
|
-
}
|
|
36
|
-
return { kind: 'wait', family, waitMs: waits.serverBusyMs };
|
|
37
|
-
case 'incomplete':
|
|
38
|
-
if (state.incompleteRetries >= budgets.maxIncompleteRetries) {
|
|
39
|
-
return { kind: 'give-up', family };
|
|
40
|
-
}
|
|
41
|
-
return { kind: 'wait', family, waitMs: waits.incompleteMs };
|
|
42
|
-
default:
|
|
43
|
-
// A generic failure is retried while the TOTAL attempt budget lasts.
|
|
44
|
-
if (state.attempt >= budgets.askRetries - 1) {
|
|
45
|
-
return { kind: 'give-up', family: 'generic' };
|
|
46
|
-
}
|
|
47
|
-
return { kind: 'generic', waitMs: 2000 * (state.attempt + 1) };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
3
|
+
// The real implementation now lives in src/retry-policy.ts (it grew the
|
|
4
|
+
// incremental-backoff mode); this module stays as the historical import
|
|
5
|
+
// point so the providers and the tests do not churn.
|
|
6
|
+
//
|
|
7
|
+
// WHY the split: the DECISION (family -> wait / compact / give-up) is pure
|
|
8
|
+
// and unit-tested, while the loop (sends, throttling, aborts) stays in the
|
|
9
|
+
// browser class. A new backend only supplies the `classify` function.
|
|
10
|
+
export { decideRetry } from './retry-policy.js';
|
package/dist/browser-login.js
CHANGED
|
@@ -53,8 +53,14 @@ export async function interactiveBrowserLogin(deps) {
|
|
|
53
53
|
await deps.persistSession().catch(() => { });
|
|
54
54
|
return 'auto';
|
|
55
55
|
}
|
|
56
|
-
// 3) A guest is usable and no window is needed
|
|
57
|
-
|
|
56
|
+
// 3) A guest is usable and no window is needed — but ONLY on a non-
|
|
57
|
+
// interactive run. MEASURED live (Qwen): a saved session that the
|
|
58
|
+
// probe cannot read yet (the app hydrates slowly, or the session lives
|
|
59
|
+
// in a cookie the probe does not look at) turned into "guest" while an
|
|
60
|
+
// OPERATOR was right there, and the run then died on the age gate.
|
|
61
|
+
// With a terminal we open the window instead: the human either sees
|
|
62
|
+
// his session already there or signs in once.
|
|
63
|
+
if (deps.guestAllowed && !deps.interactive)
|
|
58
64
|
return 'guest';
|
|
59
65
|
// 4) Non-interactive: fail fast instead of hanging on a window nobody sees.
|
|
60
66
|
if (!deps.interactive)
|
package/dist/browser.js
CHANGED
|
@@ -24,6 +24,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
|
|
|
24
24
|
// module is unchanged (tests and other modules import these names from
|
|
25
25
|
// './browser.js').
|
|
26
26
|
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, needsHeadlessUaFix, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitNotice, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
27
|
+
import { backoffWaitMs } from './retry-backoff.js';
|
|
27
28
|
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, };
|
|
28
29
|
export class DeepSeekBrowser {
|
|
29
30
|
providerName = 'deepseek';
|
|
@@ -32,6 +33,8 @@ export class DeepSeekBrowser {
|
|
|
32
33
|
channel;
|
|
33
34
|
answerTimeoutMs;
|
|
34
35
|
askRetries;
|
|
36
|
+
infiniteRetries;
|
|
37
|
+
maxRetryWaitMs;
|
|
35
38
|
stabilityChecks;
|
|
36
39
|
stabilityDelayMs;
|
|
37
40
|
minSendIntervalMs;
|
|
@@ -178,12 +181,14 @@ export class DeepSeekBrowser {
|
|
|
178
181
|
_windowClosed;
|
|
179
182
|
/** Session cookies captured before a relaunch (no `expires` -> in-memory). */
|
|
180
183
|
_carryCookies;
|
|
181
|
-
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, } = {}) {
|
|
184
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, infiniteRetries = true, maxRetryWaitMs = 1800000, 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, } = {}) {
|
|
182
185
|
this.headless = headless;
|
|
183
186
|
this.debug = debug;
|
|
184
187
|
this.channel = channel;
|
|
185
188
|
this.answerTimeoutMs = answerTimeoutMs;
|
|
186
189
|
this.askRetries = askRetries;
|
|
190
|
+
this.infiniteRetries = infiniteRetries;
|
|
191
|
+
this.maxRetryWaitMs = maxRetryWaitMs;
|
|
187
192
|
this.stabilityChecks = stabilityChecks;
|
|
188
193
|
this.stabilityDelayMs = stabilityDelayMs;
|
|
189
194
|
this.minSendIntervalMs = minSendIntervalMs;
|
|
@@ -1690,6 +1695,15 @@ export class DeepSeekBrowser {
|
|
|
1690
1695
|
async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, attachments = [], } = {}) {
|
|
1691
1696
|
let lastErr = null;
|
|
1692
1697
|
let attempt = 0;
|
|
1698
|
+
// The transient families share ONE incremental backoff (retry-backoff.ts):
|
|
1699
|
+
// with infiniteRetries on, the wait escalates instead of running out of
|
|
1700
|
+
// attempts while the server is still asking us to wait.
|
|
1701
|
+
const backoffCfg = {
|
|
1702
|
+
infiniteRetries: this.infiniteRetries,
|
|
1703
|
+
maxRetries: Math.max(1, this.maxRateLimitRetries),
|
|
1704
|
+
baseWaitMs: 5000,
|
|
1705
|
+
maxWaitMs: this.maxRetryWaitMs,
|
|
1706
|
+
};
|
|
1693
1707
|
let rateLimitRetries = 0;
|
|
1694
1708
|
let serverBusyRetries = 0;
|
|
1695
1709
|
// N8: the fallback switch is tried ONCE per ask(), not per family.
|
|
@@ -1723,9 +1737,9 @@ export class DeepSeekBrowser {
|
|
|
1723
1737
|
// The answer has settled (the finish loop returned it): mark the
|
|
1724
1738
|
// lifecycle state so the status line shows "settled" instead of
|
|
1725
1739
|
// still looking like generation.
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1740
|
+
// No 'settled' badge here: the next send flips the state right
|
|
1741
|
+
// back to 'generating', and that add/remove pair is what makes the
|
|
1742
|
+
// status row flicker. 'generating' is cleared by the caller.
|
|
1729
1743
|
return answer;
|
|
1730
1744
|
}
|
|
1731
1745
|
catch (e) {
|
|
@@ -1735,7 +1749,8 @@ export class DeepSeekBrowser {
|
|
|
1735
1749
|
// context is lost). The waits don't consume the regular ask() attempts.
|
|
1736
1750
|
if (e instanceof RateLimitError) {
|
|
1737
1751
|
rateLimitRetries++;
|
|
1738
|
-
if (
|
|
1752
|
+
if (!this.infiniteRetries &&
|
|
1753
|
+
rateLimitRetries > this.maxRateLimitRetries) {
|
|
1739
1754
|
this._notice(theme.error(this._t('ds.rate_limit_give_up', {
|
|
1740
1755
|
attempt: rateLimitRetries,
|
|
1741
1756
|
min: Math.ceil(this.rateLimitWaitMs / 60000),
|
|
@@ -1743,16 +1758,17 @@ export class DeepSeekBrowser {
|
|
|
1743
1758
|
throw e;
|
|
1744
1759
|
}
|
|
1745
1760
|
this._notice(theme.warn(this._t('ds.rate_limit_wait', {
|
|
1746
|
-
min: Math.ceil(
|
|
1761
|
+
min: Math.ceil(backoffWaitMs(backoffCfg, rateLimitRetries) / 60000),
|
|
1747
1762
|
attempt: rateLimitRetries,
|
|
1748
|
-
max: this.maxRateLimitRetries,
|
|
1763
|
+
max: this.infiniteRetries ? '∞' : this.maxRateLimitRetries,
|
|
1749
1764
|
})));
|
|
1750
1765
|
// Interruptible: Esc/Ctrl+C must cancel this long wait too,
|
|
1751
1766
|
// otherwise "stop" stays frozen for up to 5 minutes.
|
|
1752
1767
|
// The wait is ANIMATED through onSendPause (a live countdown), so the
|
|
1753
1768
|
// operator sees the agent is waiting on the rate limit instead of a
|
|
1754
1769
|
// frozen spinner. `_lastSentAt` is bumped so the countdown renders.
|
|
1755
|
-
const
|
|
1770
|
+
const waitMs = backoffWaitMs(backoffCfg, rateLimitRetries);
|
|
1771
|
+
const waitSec = Math.ceil(waitMs / 1000);
|
|
1756
1772
|
const aborted = await this._waitRateLimit(waitSec);
|
|
1757
1773
|
if (aborted)
|
|
1758
1774
|
return '(прервано пользователем)';
|
|
@@ -1764,18 +1780,20 @@ export class DeepSeekBrowser {
|
|
|
1764
1780
|
// explanation of what DeepSeek is doing.
|
|
1765
1781
|
if (e instanceof ServerBusyError) {
|
|
1766
1782
|
serverBusyRetries++;
|
|
1767
|
-
if (
|
|
1783
|
+
if (!this.infiniteRetries &&
|
|
1784
|
+
serverBusyRetries > this.maxServerBusyRetries) {
|
|
1768
1785
|
this._notice(theme.error(this._t('ds.server_busy_give_up', {
|
|
1769
1786
|
attempt: serverBusyRetries,
|
|
1770
1787
|
})));
|
|
1771
1788
|
throw e;
|
|
1772
1789
|
}
|
|
1790
|
+
const busyWaitMs = backoffWaitMs(backoffCfg, serverBusyRetries);
|
|
1773
1791
|
this._notice(theme.warn(this._t('ds.server_busy_wait', {
|
|
1774
|
-
sec: Math.ceil(
|
|
1792
|
+
sec: Math.ceil(busyWaitMs / 1000),
|
|
1775
1793
|
attempt: serverBusyRetries,
|
|
1776
|
-
max: this.maxServerBusyRetries,
|
|
1794
|
+
max: this.infiniteRetries ? '∞' : this.maxServerBusyRetries,
|
|
1777
1795
|
})));
|
|
1778
|
-
const aborted = await this._sleepInterruptible(
|
|
1796
|
+
const aborted = await this._sleepInterruptible(busyWaitMs);
|
|
1779
1797
|
if (aborted)
|
|
1780
1798
|
return '(прервано пользователем)';
|
|
1781
1799
|
continue;
|
|
@@ -1787,7 +1805,8 @@ export class DeepSeekBrowser {
|
|
|
1787
1805
|
// button does) instead of waiting for an answer that will never come.
|
|
1788
1806
|
if (e instanceof GenerationIncompleteError) {
|
|
1789
1807
|
incompleteRetries++;
|
|
1790
|
-
if (
|
|
1808
|
+
if (!this.infiniteRetries &&
|
|
1809
|
+
incompleteRetries > this.maxIncompleteRetries) {
|
|
1791
1810
|
this._notice(theme.error(this._t('ds.incomplete_give_up', {
|
|
1792
1811
|
attempt: incompleteRetries,
|
|
1793
1812
|
})));
|
|
@@ -1795,9 +1814,10 @@ export class DeepSeekBrowser {
|
|
|
1795
1814
|
}
|
|
1796
1815
|
this._notice(theme.warn(this._t('ds.incomplete_retry', {
|
|
1797
1816
|
attempt: incompleteRetries,
|
|
1798
|
-
max: this.maxIncompleteRetries,
|
|
1817
|
+
max: this.infiniteRetries ? '∞' : this.maxIncompleteRetries,
|
|
1799
1818
|
})));
|
|
1800
|
-
const
|
|
1819
|
+
const incompleteWaitMs = backoffWaitMs(backoffCfg, incompleteRetries);
|
|
1820
|
+
const aborted = await this._sleepInterruptible(incompleteWaitMs);
|
|
1801
1821
|
if (aborted)
|
|
1802
1822
|
return '(прервано пользователем)';
|
|
1803
1823
|
continue;
|
|
@@ -2025,7 +2045,12 @@ export class DeepSeekBrowser {
|
|
|
2025
2045
|
};
|
|
2026
2046
|
this._emitSendState('paused');
|
|
2027
2047
|
tick(totalSec * 1000);
|
|
2028
|
-
|
|
2048
|
+
const aborted = await this._sleepInterruptible(totalSec * 1000, tick);
|
|
2049
|
+
// The wait is over and the send is about to go out: leave the badge on
|
|
2050
|
+
// 'generating' ('paused' left it showing a paused dot forever).
|
|
2051
|
+
if (!aborted)
|
|
2052
|
+
this._emitSendState('generating');
|
|
2053
|
+
return aborted;
|
|
2029
2054
|
}
|
|
2030
2055
|
// Pause between sends. Applied ONLY to agent messages
|
|
2031
2056
|
// (tool-result, system-prompt) so we don't hit the rate limit.
|
|
@@ -2075,8 +2100,10 @@ export class DeepSeekBrowser {
|
|
|
2075
2100
|
report(gap);
|
|
2076
2101
|
this._emitSendState('paused');
|
|
2077
2102
|
await this._sleepInterruptible(gap, report);
|
|
2103
|
+
// Back to GENERATING, not settled: nothing has been sent yet, so a
|
|
2104
|
+
// "✓ done" badge here showed up before the question even left the page.
|
|
2078
2105
|
if (!this._abort && !this._askCancelled)
|
|
2079
|
-
this._emitSendState('
|
|
2106
|
+
this._emitSendState('generating');
|
|
2080
2107
|
}
|
|
2081
2108
|
// Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
|
|
2082
2109
|
// Returns true if the sleep was cut short by _abort.
|
package/dist/config-live.js
CHANGED
|
@@ -22,6 +22,8 @@ export const BROWSER_LIVE_CONFIG = {
|
|
|
22
22
|
'browser.continueMinGapMs': 'continueMinGapMs',
|
|
23
23
|
'browser.askDeadlineMs': 'askDeadlineMs',
|
|
24
24
|
'browser.maxAfterToolRetries': 'maxAfterToolRetries',
|
|
25
|
+
'browser.infiniteRetries': 'infiniteRetries',
|
|
26
|
+
'browser.maxRetryWaitMs': 'maxRetryWaitMs',
|
|
25
27
|
};
|
|
26
28
|
/** Push a browser.* change onto the live instance. Returns true when a
|
|
27
29
|
* field was written (false = not a browser timing knob, or absent on this
|
|
@@ -33,6 +35,12 @@ export function applyBrowserLiveConfig(path, value, browser) {
|
|
|
33
35
|
// A timing value must be a FINITE NON-NEGATIVE number: a NaN reached a live
|
|
34
36
|
// field (a timer with NaN fires immediately) and a negative one is nonsense.
|
|
35
37
|
// Reject the value instead of corrupting the field.
|
|
38
|
+
// A BOOLEAN toggle (browser.infiniteRetries) is a different shape: accept
|
|
39
|
+
// it as-is, otherwise every live toggle of it would be rejected here.
|
|
40
|
+
if (typeof value === 'boolean') {
|
|
41
|
+
browser[field] = value;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
36
44
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
37
45
|
return false;
|
|
38
46
|
}
|
package/dist/config-schema.js
CHANGED
|
@@ -130,6 +130,22 @@ export const CONFIG_SCHEMA = [
|
|
|
130
130
|
topKey: 'cfg.top.browser',
|
|
131
131
|
groupKey: 'cfg.group.chat',
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
path: 'browser.infiniteRetries',
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
labelKey: 'cfg.f.browser_infiniteRetries',
|
|
137
|
+
topKey: 'cfg.top.browser',
|
|
138
|
+
groupKey: 'cfg.group.recovery',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: 'browser.maxRetryWaitMs',
|
|
142
|
+
type: 'number',
|
|
143
|
+
min: 0,
|
|
144
|
+
max: 7200000,
|
|
145
|
+
labelKey: 'cfg.f.browser_maxRetryWaitMs',
|
|
146
|
+
topKey: 'cfg.top.browser',
|
|
147
|
+
groupKey: 'cfg.group.recovery',
|
|
148
|
+
},
|
|
133
149
|
{
|
|
134
150
|
path: 'browser.deepThinking',
|
|
135
151
|
type: 'boolean',
|
package/dist/config.js
CHANGED
|
@@ -110,6 +110,13 @@ export const DEFAULTS = {
|
|
|
110
110
|
// not waited out. 240s used to mean a 4-minute spinner per dead turn.
|
|
111
111
|
askDeadlineMs: 120000,
|
|
112
112
|
maxAfterToolRetries: 6,
|
|
113
|
+
// Infinite incremental retries for the transient families (rate limit,
|
|
114
|
+
// server busy, truncated turn, watchdog): the wait escalates 5m -> 10m
|
|
115
|
+
// -> 20m -> the ceiling instead of a hard attempt cap. Set false to get
|
|
116
|
+
// the old counted budget (maxRateLimitRetries / maxIncompleteRetries).
|
|
117
|
+
infiniteRetries: true,
|
|
118
|
+
// Ceiling for ONE retry wait (ms). 0 = no ceiling.
|
|
119
|
+
maxRetryWaitMs: 1800000,
|
|
113
120
|
// Auto-compact is ON by default: a provider's context window is small and
|
|
114
121
|
// per-model (GLM: 32k-128k), and running out of it mid-task is a hard stop
|
|
115
122
|
// that loses the work, while a compaction at a safe seam only costs one
|
package/dist/glm-browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { backoffWaitMs } from './retry-backoff.js';
|
|
1
2
|
// GLM (chat.z.ai) provider — the third chat backend zames can drive.
|
|
2
3
|
//
|
|
3
4
|
// WHY a separate class (the same reason QwenBrowser exists): the backends
|
|
@@ -57,6 +58,8 @@ export class GlmBrowser {
|
|
|
57
58
|
channel;
|
|
58
59
|
answerTimeoutMs;
|
|
59
60
|
askRetries;
|
|
61
|
+
infiniteRetries;
|
|
62
|
+
maxRetryWaitMs;
|
|
60
63
|
stabilityChecks;
|
|
61
64
|
stabilityDelayMs;
|
|
62
65
|
minSendIntervalMs;
|
|
@@ -131,12 +134,14 @@ export class GlmBrowser {
|
|
|
131
134
|
* removes most of the wait. Empty = leave the site's default.
|
|
132
135
|
*/
|
|
133
136
|
reasoningEffort;
|
|
134
|
-
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, } = {}) {
|
|
137
|
+
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 300000, askRetries = 3, infiniteRetries = true, maxRetryWaitMs = 1800000, 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, } = {}) {
|
|
135
138
|
this.headless = headless;
|
|
136
139
|
this.debug = debug;
|
|
137
140
|
this.channel = channel;
|
|
138
141
|
this.answerTimeoutMs = answerTimeoutMs;
|
|
139
142
|
this.askRetries = askRetries;
|
|
143
|
+
this.infiniteRetries = infiniteRetries;
|
|
144
|
+
this.maxRetryWaitMs = maxRetryWaitMs;
|
|
140
145
|
this.stabilityChecks = stabilityChecks;
|
|
141
146
|
this.stabilityDelayMs = stabilityDelayMs;
|
|
142
147
|
this.minSendIntervalMs = minSendIntervalMs;
|
|
@@ -653,7 +658,7 @@ export class GlmBrowser {
|
|
|
653
658
|
*/
|
|
654
659
|
async waitForLogin() {
|
|
655
660
|
const interactive = loginIsInteractive({
|
|
656
|
-
isTTY: !!process.stdin.isTTY,
|
|
661
|
+
isTTY: !!process.stdin.isTTY || !this.headless,
|
|
657
662
|
oneShot: false,
|
|
658
663
|
scheduled: false,
|
|
659
664
|
env: process.env,
|
|
@@ -1426,6 +1431,10 @@ export class GlmBrowser {
|
|
|
1426
1431
|
report(gap);
|
|
1427
1432
|
this._emitSendState('paused');
|
|
1428
1433
|
await this._sleepInterruptible(gap, report);
|
|
1434
|
+
// Back to GENERATING: nothing has been sent yet, so a "done" badge here
|
|
1435
|
+
// would show up before the question even left the page.
|
|
1436
|
+
if (!this._abort && !this._askCancelled)
|
|
1437
|
+
this._emitSendState('generating');
|
|
1429
1438
|
}
|
|
1430
1439
|
async _sleepInterruptible(ms, onTick) {
|
|
1431
1440
|
const step = 100;
|
|
@@ -1455,6 +1464,12 @@ export class GlmBrowser {
|
|
|
1455
1464
|
async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, attachments = [], } = {}) {
|
|
1456
1465
|
let lastErr = null;
|
|
1457
1466
|
let attempt = 0;
|
|
1467
|
+
const backoffCfg = {
|
|
1468
|
+
infiniteRetries: this.infiniteRetries,
|
|
1469
|
+
maxRetries: Math.max(1, this.maxRateLimitRetries),
|
|
1470
|
+
baseWaitMs: 5000,
|
|
1471
|
+
maxWaitMs: this.maxRetryWaitMs,
|
|
1472
|
+
};
|
|
1458
1473
|
let rateLimitRetries = 0;
|
|
1459
1474
|
let serverBusyRetries = 0;
|
|
1460
1475
|
let incompleteRetries = 0;
|
|
@@ -1465,16 +1480,16 @@ export class GlmBrowser {
|
|
|
1465
1480
|
let fallbackAttempted = false;
|
|
1466
1481
|
this._askInFlight++;
|
|
1467
1482
|
try {
|
|
1468
|
-
while (attempt < this.askRetries) {
|
|
1483
|
+
while (this.infiniteRetries || attempt < this.askRetries) {
|
|
1469
1484
|
try {
|
|
1470
1485
|
const answer = await this._askOnce(prompt, {
|
|
1471
1486
|
timeout,
|
|
1472
1487
|
agent,
|
|
1473
1488
|
attachments,
|
|
1474
1489
|
});
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1490
|
+
// No transient 'settled' here: the next send flips the state
|
|
1491
|
+
// back to 'generating' and that add/remove pair is the status-row
|
|
1492
|
+
// flicker (same fix as the DeepSeek provider).
|
|
1478
1493
|
return answer;
|
|
1479
1494
|
}
|
|
1480
1495
|
catch (e) {
|
|
@@ -1517,42 +1532,45 @@ export class GlmBrowser {
|
|
|
1517
1532
|
}
|
|
1518
1533
|
if (/rate limit|too frequent/i.test(msg)) {
|
|
1519
1534
|
this.sendRuntime.onOutcome('rate_limit', { errorText: msg });
|
|
1520
|
-
if (
|
|
1535
|
+
if (!this.infiniteRetries &&
|
|
1536
|
+
rateLimitRetries >= this.maxRateLimitRetries)
|
|
1521
1537
|
break;
|
|
1522
1538
|
rateLimitRetries++;
|
|
1523
1539
|
this._notice(this._t('glm.rate_limit_wait', {
|
|
1524
|
-
min: Math.ceil(
|
|
1540
|
+
min: Math.ceil(backoffWaitMs(backoffCfg, rateLimitRetries) / 60000),
|
|
1525
1541
|
attempt: rateLimitRetries,
|
|
1526
|
-
max: this.maxRateLimitRetries,
|
|
1542
|
+
max: this.infiniteRetries ? '∞' : this.maxRateLimitRetries,
|
|
1527
1543
|
}));
|
|
1528
|
-
if (await this._sleepInterruptible(
|
|
1544
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, rateLimitRetries)))
|
|
1529
1545
|
return '(прервано пользователем)';
|
|
1530
1546
|
continue;
|
|
1531
1547
|
}
|
|
1532
1548
|
if (e instanceof GlmServerBusyError || /server busy/i.test(msg)) {
|
|
1533
1549
|
this.sendRuntime.onOutcome('busy', { errorText: msg });
|
|
1534
|
-
if (
|
|
1550
|
+
if (!this.infiniteRetries &&
|
|
1551
|
+
serverBusyRetries >= this.maxServerBusyRetries)
|
|
1535
1552
|
break;
|
|
1536
1553
|
serverBusyRetries++;
|
|
1537
1554
|
this._notice(this._t('glm.server_busy_wait', {
|
|
1538
|
-
sec: Math.ceil(
|
|
1555
|
+
sec: Math.ceil(backoffWaitMs(backoffCfg, serverBusyRetries) / 1000),
|
|
1539
1556
|
attempt: serverBusyRetries,
|
|
1540
|
-
max: this.maxServerBusyRetries,
|
|
1557
|
+
max: this.infiniteRetries ? '∞' : this.maxServerBusyRetries,
|
|
1541
1558
|
}));
|
|
1542
|
-
if (await this._sleepInterruptible(
|
|
1559
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, serverBusyRetries)))
|
|
1543
1560
|
return '(прервано пользователем)';
|
|
1544
1561
|
continue;
|
|
1545
1562
|
}
|
|
1546
1563
|
if (e instanceof GlmIncompleteError) {
|
|
1547
1564
|
this.sendRuntime.onOutcome('incomplete', { errorText: msg });
|
|
1548
|
-
if (
|
|
1565
|
+
if (!this.infiniteRetries &&
|
|
1566
|
+
incompleteRetries >= this.maxIncompleteRetries)
|
|
1549
1567
|
break;
|
|
1550
1568
|
incompleteRetries++;
|
|
1551
1569
|
this._notice(this._t('glm.incomplete_retry', {
|
|
1552
1570
|
attempt: incompleteRetries,
|
|
1553
|
-
max: this.maxIncompleteRetries,
|
|
1571
|
+
max: this.infiniteRetries ? '∞' : this.maxIncompleteRetries,
|
|
1554
1572
|
}));
|
|
1555
|
-
if (await this._sleepInterruptible(
|
|
1573
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, incompleteRetries)))
|
|
1556
1574
|
return '(прервано пользователем)';
|
|
1557
1575
|
continue;
|
|
1558
1576
|
}
|
package/dist/i18n/config.js
CHANGED
|
@@ -230,6 +230,14 @@ export const configMessages = {
|
|
|
230
230
|
ru: 'Повторов после таймаута',
|
|
231
231
|
en: 'Retries after a timeout',
|
|
232
232
|
},
|
|
233
|
+
'cfg.f.browser_infiniteRetries': {
|
|
234
|
+
ru: 'Повторять бесконечно (с растущей паузой)',
|
|
235
|
+
en: 'Retry forever (with an escalating wait)',
|
|
236
|
+
},
|
|
237
|
+
'cfg.f.browser_maxRetryWaitMs': {
|
|
238
|
+
ru: 'Потолок одной паузы перед повтором, мс',
|
|
239
|
+
en: 'Ceiling for one retry wait, ms',
|
|
240
|
+
},
|
|
233
241
|
'cfg.f.browser_continueMinGapMs': {
|
|
234
242
|
ru: 'Мин. пауза перед нажатием Continue, мс',
|
|
235
243
|
en: 'Min gap before a Continue click, ms',
|
package/dist/index.js
CHANGED
|
@@ -1362,6 +1362,8 @@ async function main() {
|
|
|
1362
1362
|
maxSubagents: config.browser.maxSubagents,
|
|
1363
1363
|
askDeadlineMs: providerAskDeadlineMs(config),
|
|
1364
1364
|
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
1365
|
+
infiniteRetries: config.browser.infiniteRetries,
|
|
1366
|
+
maxRetryWaitMs: config.browser.maxRetryWaitMs,
|
|
1365
1367
|
// An `ask` permission rule must also gate the subagent: otherwise a
|
|
1366
1368
|
// guarded tool (e.g. a dangerous Bash command) would run inside the
|
|
1367
1369
|
// subagent WITHOUT the operator's confirmation.
|
package/dist/qwen-browser.js
CHANGED
|
@@ -28,6 +28,7 @@ import { theme } from './theme.js';
|
|
|
28
28
|
import { recoverFromContextLimit } from './context-limit.js';
|
|
29
29
|
import { scrapeQwenChatList, scrapeQwenChatMessages, parseQwenChatList, parseQwenHistory, } from './qwen-chats.js';
|
|
30
30
|
import { cleanAnswer, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, sanitizeHeadlessUA, needsHeadlessUaFix, cleanSingletonFiles, profileLooksLocked, nukeProfile, writeAuthMarker, } from './browser-pure.js';
|
|
31
|
+
import { backoffWaitMs } from './retry-backoff.js';
|
|
31
32
|
import { chatSignalInDom, dumpDomCountsInDom, isElementEnabledInDom, readInputTrimmedTextInDom, readLastAnswerInDom, readPageToastsInDom, readUserAgentInDom, setBusyTitleInDom, clickStopInDom, stopButtonVisibleInDom, } from './browser-dom.js';
|
|
32
33
|
import { probeUiHealth, } from './ui-health.js';
|
|
33
34
|
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';
|
|
@@ -71,6 +72,8 @@ export class QwenBrowser {
|
|
|
71
72
|
channel;
|
|
72
73
|
answerTimeoutMs;
|
|
73
74
|
askRetries;
|
|
75
|
+
infiniteRetries;
|
|
76
|
+
maxRetryWaitMs;
|
|
74
77
|
stabilityChecks;
|
|
75
78
|
stabilityDelayMs;
|
|
76
79
|
minSendIntervalMs;
|
|
@@ -148,12 +151,14 @@ export class QwenBrowser {
|
|
|
148
151
|
onContextLimit;
|
|
149
152
|
/** N8: switch to an authorized fallback provider (see agent-loop). */
|
|
150
153
|
onProviderExhausted = null;
|
|
151
|
-
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, } = {}) {
|
|
154
|
+
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, infiniteRetries = true, maxRetryWaitMs = 1800000, 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, } = {}) {
|
|
152
155
|
this.headless = headless;
|
|
153
156
|
this.debug = debug;
|
|
154
157
|
this.channel = channel;
|
|
155
158
|
this.answerTimeoutMs = answerTimeoutMs;
|
|
156
159
|
this.askRetries = askRetries;
|
|
160
|
+
this.infiniteRetries = infiniteRetries;
|
|
161
|
+
this.maxRetryWaitMs = maxRetryWaitMs;
|
|
157
162
|
this.stabilityChecks = stabilityChecks;
|
|
158
163
|
this.stabilityDelayMs = stabilityDelayMs;
|
|
159
164
|
this.minSendIntervalMs = minSendIntervalMs;
|
|
@@ -586,29 +591,44 @@ export class QwenBrowser {
|
|
|
586
591
|
async probeLogin() {
|
|
587
592
|
if (!this.page)
|
|
588
593
|
return 'unknown';
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
594
|
+
// MEASURED live: on a freshly navigated page the React app has not
|
|
595
|
+
// hydrated yet, so localStorage is still EMPTY and the guest CTA is
|
|
596
|
+
// painted. That made a genuinely signed-in session report "out"; the
|
|
597
|
+
// headless run continued as a GUEST and every send hit Qwen's
|
|
598
|
+
// "Confirm your age" gate. Wait for the app to write its tokens
|
|
599
|
+
// (`token` / `qwen_access_token_state`, seen on the live profile).
|
|
600
|
+
const hydrationDeadline = Date.now() + 20_000;
|
|
601
|
+
while (Date.now() < hydrationDeadline) {
|
|
602
|
+
try {
|
|
603
|
+
const hasToken = await this.page.evaluate(() => {
|
|
592
604
|
try {
|
|
593
|
-
return !!localStorage.getItem('token')
|
|
605
|
+
return !!(localStorage.getItem('token') ||
|
|
606
|
+
localStorage.getItem('qwen_access_token_state'));
|
|
594
607
|
}
|
|
595
608
|
catch {
|
|
596
609
|
return false;
|
|
597
610
|
}
|
|
598
|
-
})
|
|
611
|
+
});
|
|
612
|
+
if (hasToken)
|
|
613
|
+
return 'in';
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// the page may be mid-navigation: retry on the next tick
|
|
617
|
+
}
|
|
618
|
+
await this.page.waitForTimeout(500);
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const signInVisible = await this.page.evaluate(() => {
|
|
599
622
|
const body = document.body ? document.body.innerText : '';
|
|
600
|
-
|
|
601
|
-
return { hasToken, signInVisible };
|
|
623
|
+
return /\b(log ?in|sign ?in)\b/i.test(body);
|
|
602
624
|
});
|
|
603
|
-
if (
|
|
604
|
-
return 'in';
|
|
605
|
-
if (r.signInVisible)
|
|
625
|
+
if (signInVisible)
|
|
606
626
|
return 'out';
|
|
607
|
-
return 'unknown';
|
|
608
627
|
}
|
|
609
628
|
catch {
|
|
610
|
-
|
|
629
|
+
// fall through to unknown
|
|
611
630
|
}
|
|
631
|
+
return 'unknown';
|
|
612
632
|
}
|
|
613
633
|
/**
|
|
614
634
|
* A human signs in inside a VISIBLE window (a one-time code cannot be typed
|
|
@@ -769,7 +789,10 @@ export class QwenBrowser {
|
|
|
769
789
|
// guest, so no error) the guest notice. The visible window and the return
|
|
770
790
|
// to headless are the SHARED interactiveBrowserLogin path.
|
|
771
791
|
const interactive = loginIsInteractive({
|
|
772
|
-
|
|
792
|
+
// A HEADED run IS interactive: the visible window is exactly what the
|
|
793
|
+
// human needs for the one-time code, even when stdin is a pipe (a smoke
|
|
794
|
+
// script). Requiring a TTY there failed the run with LoginRequired.
|
|
795
|
+
isTTY: !!(process.stdin.isTTY && process.stdout.isTTY) || !this.headless,
|
|
773
796
|
oneShot: false,
|
|
774
797
|
scheduled: false,
|
|
775
798
|
env: process.env,
|
|
@@ -811,15 +834,19 @@ export class QwenBrowser {
|
|
|
811
834
|
now: () => Date.now(),
|
|
812
835
|
interactive,
|
|
813
836
|
stayHeaded: !this.headless,
|
|
814
|
-
// A guest is usable
|
|
815
|
-
//
|
|
816
|
-
|
|
837
|
+
// A guest is usable ONLY when nobody can sign in: with a terminal the
|
|
838
|
+
// window is opened instead (a slow-hydrating session must not be
|
|
839
|
+
// mistaken for a guest — that turned a signed-in run into a guest and
|
|
840
|
+
// Qwen's age gate then blocked every send).
|
|
841
|
+
guestAllowed: false,
|
|
817
842
|
canShowWindow: canShowWindow(process.env, process.platform),
|
|
818
843
|
tuiLogin: this.onLoginPrompt
|
|
819
844
|
? async () => await this._promptAndLogin()
|
|
820
845
|
: undefined,
|
|
821
846
|
timeoutMs: this.loginTimeoutMs,
|
|
822
847
|
});
|
|
848
|
+
if (process.env.ZAMES_LOGIN_DEBUG)
|
|
849
|
+
console.error('qwen outcome=' + outcome);
|
|
823
850
|
if (outcome === 'guest')
|
|
824
851
|
this._notice(this._t('qwen.guest_mode'));
|
|
825
852
|
await this._persistSessionIfNeeded().catch(() => { });
|
|
@@ -1680,6 +1707,10 @@ export class QwenBrowser {
|
|
|
1680
1707
|
report(gap);
|
|
1681
1708
|
this._emitSendState('paused');
|
|
1682
1709
|
await this._sleepInterruptible(gap, report);
|
|
1710
|
+
// Back to GENERATING: nothing has been sent yet, so a "done" badge here
|
|
1711
|
+
// would show up before the question even left the page.
|
|
1712
|
+
if (!this._abort && !this._askCancelled)
|
|
1713
|
+
this._emitSendState('generating');
|
|
1683
1714
|
}
|
|
1684
1715
|
async _sleepInterruptible(ms, onTick) {
|
|
1685
1716
|
const step = 100;
|
|
@@ -1711,6 +1742,12 @@ export class QwenBrowser {
|
|
|
1711
1742
|
async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, attachments = [], } = {}) {
|
|
1712
1743
|
let lastErr = null;
|
|
1713
1744
|
let attempt = 0;
|
|
1745
|
+
const backoffCfg = {
|
|
1746
|
+
infiniteRetries: this.infiniteRetries,
|
|
1747
|
+
maxRetries: Math.max(1, this.maxRateLimitRetries),
|
|
1748
|
+
baseWaitMs: 5000,
|
|
1749
|
+
maxWaitMs: this.maxRetryWaitMs,
|
|
1750
|
+
};
|
|
1714
1751
|
let rateLimitRetries = 0;
|
|
1715
1752
|
let serverBusyRetries = 0;
|
|
1716
1753
|
let incompleteRetries = 0;
|
|
@@ -1721,16 +1758,16 @@ export class QwenBrowser {
|
|
|
1721
1758
|
let fallbackAttempted = false;
|
|
1722
1759
|
this._askInFlight++;
|
|
1723
1760
|
try {
|
|
1724
|
-
while (attempt < this.askRetries) {
|
|
1761
|
+
while (this.infiniteRetries || attempt < this.askRetries) {
|
|
1725
1762
|
try {
|
|
1726
1763
|
const answer = await this._askOnce(prompt, {
|
|
1727
1764
|
timeout,
|
|
1728
1765
|
agent,
|
|
1729
1766
|
attachments,
|
|
1730
1767
|
});
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1768
|
+
// No transient 'settled' here: the next send flips the state
|
|
1769
|
+
// back to 'generating' and that add/remove pair is the status-row
|
|
1770
|
+
// flicker (same fix as the DeepSeek provider).
|
|
1734
1771
|
return answer;
|
|
1735
1772
|
}
|
|
1736
1773
|
catch (e) {
|
|
@@ -1773,40 +1810,43 @@ export class QwenBrowser {
|
|
|
1773
1810
|
continue;
|
|
1774
1811
|
}
|
|
1775
1812
|
if (/rate limit|too frequent/i.test(msg)) {
|
|
1776
|
-
if (
|
|
1813
|
+
if (!this.infiniteRetries &&
|
|
1814
|
+
rateLimitRetries >= this.maxRateLimitRetries)
|
|
1777
1815
|
break;
|
|
1778
1816
|
rateLimitRetries++;
|
|
1779
1817
|
this._notice(this._t('qwen.rate_limit_wait', {
|
|
1780
|
-
min: Math.ceil(
|
|
1818
|
+
min: Math.ceil(backoffWaitMs(backoffCfg, rateLimitRetries) / 60000),
|
|
1781
1819
|
attempt: rateLimitRetries,
|
|
1782
|
-
max: this.maxRateLimitRetries,
|
|
1820
|
+
max: this.infiniteRetries ? '∞' : this.maxRateLimitRetries,
|
|
1783
1821
|
}));
|
|
1784
|
-
if (await this._sleepInterruptible(
|
|
1822
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, rateLimitRetries)))
|
|
1785
1823
|
return '(прервано пользователем)';
|
|
1786
1824
|
continue;
|
|
1787
1825
|
}
|
|
1788
1826
|
if (e instanceof QwenServerBusyError || /server busy/i.test(msg)) {
|
|
1789
|
-
if (
|
|
1827
|
+
if (!this.infiniteRetries &&
|
|
1828
|
+
serverBusyRetries >= this.maxServerBusyRetries)
|
|
1790
1829
|
break;
|
|
1791
1830
|
serverBusyRetries++;
|
|
1792
1831
|
this._notice(this._t('qwen.server_busy_wait', {
|
|
1793
|
-
sec: Math.ceil(
|
|
1832
|
+
sec: Math.ceil(backoffWaitMs(backoffCfg, serverBusyRetries) / 1000),
|
|
1794
1833
|
attempt: serverBusyRetries,
|
|
1795
|
-
max: this.maxServerBusyRetries,
|
|
1834
|
+
max: this.infiniteRetries ? '∞' : this.maxServerBusyRetries,
|
|
1796
1835
|
}));
|
|
1797
|
-
if (await this._sleepInterruptible(
|
|
1836
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, serverBusyRetries)))
|
|
1798
1837
|
return '(прервано пользователем)';
|
|
1799
1838
|
continue;
|
|
1800
1839
|
}
|
|
1801
1840
|
if (e instanceof QwenIncompleteError) {
|
|
1802
|
-
if (
|
|
1841
|
+
if (!this.infiniteRetries &&
|
|
1842
|
+
incompleteRetries >= this.maxIncompleteRetries)
|
|
1803
1843
|
break;
|
|
1804
1844
|
incompleteRetries++;
|
|
1805
1845
|
this._notice(theme.warn(this._t('qwen.incomplete_retry', {
|
|
1806
1846
|
attempt: incompleteRetries,
|
|
1807
|
-
max: this.maxIncompleteRetries,
|
|
1847
|
+
max: this.infiniteRetries ? '∞' : this.maxIncompleteRetries,
|
|
1808
1848
|
})));
|
|
1809
|
-
if (await this._sleepInterruptible(
|
|
1849
|
+
if (await this._sleepInterruptible(backoffWaitMs(backoffCfg, incompleteRetries)))
|
|
1810
1850
|
return '(прервано пользователем)';
|
|
1811
1851
|
continue;
|
|
1812
1852
|
}
|
package/dist/restore-history.js
CHANGED
|
@@ -120,6 +120,31 @@ export function normalizeRestoredMessage(m) {
|
|
|
120
120
|
// copies after a flaky run), so drop it like the other nudges.
|
|
121
121
|
if (/^The previous turn was cut off/.test(text))
|
|
122
122
|
return null;
|
|
123
|
+
// The OTHER loop nudges (a malformed call, a plain-text answer, "the work
|
|
124
|
+
// is NOT finished", "you stopped mid-task", a repeated call) are written by
|
|
125
|
+
// the agent to itself in the same way. A flaky run left a wall of these in
|
|
126
|
+
// /chats: the operator saw them as their OWN messages ("❯ Вы: Your previous
|
|
127
|
+
// answer was not recognized…") and thought the terminal history was wrong.
|
|
128
|
+
if (/^Your previous answer was not recognized as a tool call/.test(text))
|
|
129
|
+
return null;
|
|
130
|
+
if (/^Plain text is a PROTOCOL VIOLATION/.test(text))
|
|
131
|
+
return null;
|
|
132
|
+
if (/^You wrote plain text without a tool call/.test(text))
|
|
133
|
+
return null;
|
|
134
|
+
if (/^You stopped after a tool call and wrote plain text/.test(text))
|
|
135
|
+
return null;
|
|
136
|
+
if (/^You stopped mid-task without finishing/.test(text))
|
|
137
|
+
return null;
|
|
138
|
+
if (/^Last step: call the respond tool/.test(text))
|
|
139
|
+
return null;
|
|
140
|
+
if (/^You have called the SAME tool with the SAME arguments/.test(text))
|
|
141
|
+
return null;
|
|
142
|
+
if (/^It looks like you meant to call a tool but did not/.test(text))
|
|
143
|
+
return null;
|
|
144
|
+
if (/^Continue the task\. If you need a tool/.test(text))
|
|
145
|
+
return null;
|
|
146
|
+
if (/^Before finishing: you wrote\/edited a runnable code file/.test(text))
|
|
147
|
+
return null;
|
|
123
148
|
if (/^\[system\]/.test(text))
|
|
124
149
|
return null;
|
|
125
150
|
if (/^The user ran \//.test(text))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Incremental RETRY BACKOFF — how long to wait before a retry of a
|
|
2
|
+
// transient failure (rate limit / server busy / truncated turn / watchdog
|
|
3
|
+
// timeout).
|
|
4
|
+
//
|
|
5
|
+
// WHY: the old per-provider loop had a FIXED pause per family and a hard
|
|
6
|
+
// retry ceiling. A long block (DeepSeek answers "too frequent" for minutes)
|
|
7
|
+
// then failed the very first retry at the same fixed pause, burned the whole
|
|
8
|
+
// budget on cheap retries and surrendered while the block was still active —
|
|
9
|
+
// the operator's «лимит повторов исчерпан». Every retry must wait LONGER than
|
|
10
|
+
// the previous one (up to a ceiling), and the budget must not run out while
|
|
11
|
+
// the server is still asking us to wait.
|
|
12
|
+
//
|
|
13
|
+
// PURE: the caller owns the counter and the clock; this module only decides
|
|
14
|
+
// the next wait and whether the family may be retried again.
|
|
15
|
+
export const DEFAULT_BACKOFF = {
|
|
16
|
+
infiniteRetries: true,
|
|
17
|
+
maxRetries: 6,
|
|
18
|
+
baseWaitMs: 5 * 60_000,
|
|
19
|
+
// Half an hour is where a wait stops being a pause and becomes a dead run:
|
|
20
|
+
// the operator can restart instead of watching a spinner for an hour.
|
|
21
|
+
maxWaitMs: 30 * 60_000,
|
|
22
|
+
};
|
|
23
|
+
/** A wait never drops below this: a server-busy wait of 0 would retry in a
|
|
24
|
+
* tight loop against a server that just told us it is overloaded. */
|
|
25
|
+
const MIN_WAIT_MS = 500;
|
|
26
|
+
/** Double on every retry: 5m, 10m, 20m, 30m, 30m, 30m... */
|
|
27
|
+
export const BACKOFF_FACTOR = 2;
|
|
28
|
+
// +/-10% jitter: several agents (or several families in one agent) must not
|
|
29
|
+
// retry in the same second and trip the server's burst detector again.
|
|
30
|
+
const JITTER_RATIO = 0.1;
|
|
31
|
+
/** A ceiling of 0 or Infinity means "no ceiling" (a hand-edited config). */
|
|
32
|
+
export function normalizeMaxWait(maxWaitMs) {
|
|
33
|
+
return Number.isFinite(maxWaitMs) && maxWaitMs > 0 ? maxWaitMs : 0;
|
|
34
|
+
}
|
|
35
|
+
/** Clamp one wait to the configured ceiling. */
|
|
36
|
+
export function clampWait(waitMs, maxWaitMs) {
|
|
37
|
+
if (!(Number.isFinite(waitMs) && waitMs > 0))
|
|
38
|
+
return 0;
|
|
39
|
+
const cap = normalizeMaxWait(maxWaitMs);
|
|
40
|
+
return cap ? Math.min(waitMs, cap) : waitMs;
|
|
41
|
+
}
|
|
42
|
+
/** Shortest acceptable pause. */
|
|
43
|
+
export function floorWait(waitMs) {
|
|
44
|
+
if (!(Number.isFinite(waitMs) && waitMs > 0))
|
|
45
|
+
return 0;
|
|
46
|
+
return Math.max(MIN_WAIT_MS, waitMs);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* PURE: the wait before the next retry, `attempt` being 1-based (1 = the
|
|
50
|
+
* first retry, i.e. after the first failure).
|
|
51
|
+
*
|
|
52
|
+
* `serverWaitMs` (a Retry-After) WINS over the learned curve: when the server
|
|
53
|
+
* names its own pause, guessing a shorter one only burns another attempt.
|
|
54
|
+
* It is still clamped to the ceiling, and it never goes below the curve, so
|
|
55
|
+
* a tiny Retry-After cannot shorten the escalation.
|
|
56
|
+
*/
|
|
57
|
+
export function backoffWaitMs(cfg, attempt, opts = {}) {
|
|
58
|
+
const n = Math.max(1, Math.floor(attempt));
|
|
59
|
+
const base = Number.isFinite(cfg.baseWaitMs) && cfg.baseWaitMs > 0
|
|
60
|
+
? cfg.baseWaitMs
|
|
61
|
+
: DEFAULT_BACKOFF.baseWaitMs;
|
|
62
|
+
let wait = base * Math.pow(BACKOFF_FACTOR, n - 1);
|
|
63
|
+
if (!Number.isFinite(wait))
|
|
64
|
+
wait = Number.MAX_SAFE_INTEGER;
|
|
65
|
+
const server = opts.serverWaitMs;
|
|
66
|
+
if (typeof server === 'number' && Number.isFinite(server) && server > 0) {
|
|
67
|
+
wait = Math.max(wait, server);
|
|
68
|
+
}
|
|
69
|
+
wait = clampWait(wait, cfg.maxWaitMs);
|
|
70
|
+
if (opts.jitter !== false) {
|
|
71
|
+
const spread = Math.round(wait * JITTER_RATIO);
|
|
72
|
+
if (spread > 0)
|
|
73
|
+
wait += Math.round((Math.random() * 2 - 1) * spread);
|
|
74
|
+
}
|
|
75
|
+
return Math.max(MIN_WAIT_MS, Math.round(wait));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* PURE: may the family be retried once more after `attemptsMade` failures?
|
|
79
|
+
* With `infiniteRetries` on the answer is always yes — the escalation and
|
|
80
|
+
* the cancel path are what keep an endless wait honest, not a counter.
|
|
81
|
+
*/
|
|
82
|
+
export function canRetry(cfg, attemptsMade) {
|
|
83
|
+
if (cfg.infiniteRetries === true)
|
|
84
|
+
return true;
|
|
85
|
+
const max = Math.max(0, Math.floor(cfg.maxRetries));
|
|
86
|
+
if (max === 0)
|
|
87
|
+
return true;
|
|
88
|
+
return attemptsMade < max;
|
|
89
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The retry POLICY of a provider's ask() as a PURE function.
|
|
2
|
+
//
|
|
3
|
+
// WHY: the three providers ran the same loop — call _askOnce, classify the
|
|
4
|
+
// thrown error into a family, retry that family within its own budget — and
|
|
5
|
+
// each copy had its own order of checks and its own wording. The loop itself
|
|
6
|
+
// (sends, throttling, aborts) stays in the browser class; only the DECISION is
|
|
7
|
+
// here, so every branch is unit-tested without a browser.
|
|
8
|
+
//
|
|
9
|
+
// Incremental retries (default): the three transient families (and the
|
|
10
|
+
// agent-loop watchdog) no longer count their attempts; the WAIT grows from the
|
|
11
|
+
// family's base (rate limit: 5m, busy: 3s, incomplete: 2s) up to the ceiling
|
|
12
|
+
// (browser.maxRetryWaitMs, default 30m), and the only way out is a cancel
|
|
13
|
+
// (Esc/Ctrl+C) or a FATAL error (bad login, captcha, guest limit) — the same
|
|
14
|
+
// place it was before.
|
|
15
|
+
//
|
|
16
|
+
// One thing matters for understanding this module: a retry that waits the
|
|
17
|
+
// SAME fixed pause forever is not endless, it is a busy loop that never lets
|
|
18
|
+
// the block expire. The escalation is what makes "infinite" safe.
|
|
19
|
+
import { backoffWaitMs, canRetry, } from './retry-backoff.js';
|
|
20
|
+
/**
|
|
21
|
+
* Decide what to do after a failed attempt.
|
|
22
|
+
*
|
|
23
|
+
* classify maps the thrown error to a family; it lives in the provider
|
|
24
|
+
* because the backend-specific markers (guest limit, age gate, captcha) are
|
|
25
|
+
* provider knowledge. A null classification falls through to the generic
|
|
26
|
+
* attempt budget.
|
|
27
|
+
*
|
|
28
|
+
* backoff carries the retry mode (infiniteRetries, the ceiling) — see
|
|
29
|
+
* src/retry-backoff.ts.
|
|
30
|
+
*/
|
|
31
|
+
export function decideRetry(family, state, budgets, waits, backoff, opts = {}) {
|
|
32
|
+
// Strict === true: the infinite mode is opt-in by an EXPLICIT config field.
|
|
33
|
+
// A caller that omits it keeps the historical counted budget, so a
|
|
34
|
+
// partially built config can never silently retry forever.
|
|
35
|
+
const infinite = backoff?.infiniteRetries === true; // strict
|
|
36
|
+
const cfg = {
|
|
37
|
+
infiniteRetries: infinite,
|
|
38
|
+
maxRetries: backoff?.maxRetries ?? 6,
|
|
39
|
+
baseWaitMs: waits.rateLimitMs,
|
|
40
|
+
maxWaitMs: backoff?.maxWaitMs ?? 0,
|
|
41
|
+
};
|
|
42
|
+
// The cause of a transient failure decides the FIRST wait — a rate block is
|
|
43
|
+
// waited out for minutes, a transient busy notice for seconds — and the
|
|
44
|
+
// escalation adds to it from there.
|
|
45
|
+
const baseWait = (f) => f === 'server-busy'
|
|
46
|
+
? waits.serverBusyMs
|
|
47
|
+
: f === 'incomplete'
|
|
48
|
+
? waits.incompleteMs
|
|
49
|
+
: waits.rateLimitMs;
|
|
50
|
+
const waitFor = (f, attempts) => backoffWaitMs({ ...cfg, baseWaitMs: baseWait(f) }, attempts, {
|
|
51
|
+
// A server Retry-After is the best clock we have; it is still clamped
|
|
52
|
+
// to the ceiling and never shortens the escalation.
|
|
53
|
+
serverWaitMs: f === 'rate-limit' ? waits.retryAfterMs : undefined,
|
|
54
|
+
jitter: opts.jitter,
|
|
55
|
+
});
|
|
56
|
+
switch (family) {
|
|
57
|
+
case 'fatal':
|
|
58
|
+
return { kind: 'give-up', family };
|
|
59
|
+
case 'context-limit':
|
|
60
|
+
if (state.contextLimitRetries >= budgets.maxContextLimitRetries) {
|
|
61
|
+
return { kind: 'give-up', family };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'compact', family };
|
|
64
|
+
case 'rate-limit':
|
|
65
|
+
if (!canRetry(cfg, state.rateLimitRetries)) {
|
|
66
|
+
return { kind: 'give-up', family };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
kind: 'wait',
|
|
70
|
+
family,
|
|
71
|
+
waitMs: waitFor('rate-limit', state.rateLimitRetries + 1),
|
|
72
|
+
};
|
|
73
|
+
case 'server-busy':
|
|
74
|
+
if (!canRetry(cfg, state.serverBusyRetries)) {
|
|
75
|
+
return { kind: 'give-up', family };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
kind: 'wait',
|
|
79
|
+
family,
|
|
80
|
+
waitMs: waitFor('server-busy', state.serverBusyRetries + 1),
|
|
81
|
+
};
|
|
82
|
+
case 'incomplete':
|
|
83
|
+
if (!canRetry(cfg, state.incompleteRetries)) {
|
|
84
|
+
return { kind: 'give-up', family };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
kind: 'wait',
|
|
88
|
+
family,
|
|
89
|
+
waitMs: waitFor('incomplete', state.incompleteRetries + 1),
|
|
90
|
+
};
|
|
91
|
+
default:
|
|
92
|
+
// A generic failure is retried while the TOTAL attempt budget lasts.
|
|
93
|
+
if (state.attempt >= budgets.askRetries - 1) {
|
|
94
|
+
return { kind: 'give-up', family: 'generic' };
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'generic', waitMs: 2000 * (state.attempt + 1) };
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/run-task.js
CHANGED
|
@@ -406,6 +406,8 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
406
406
|
locale,
|
|
407
407
|
askDeadlineMs: providerAskDeadlineMs(config),
|
|
408
408
|
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
409
|
+
infiniteRetries: config.browser.infiniteRetries,
|
|
410
|
+
maxRetryWaitMs: config.browser.maxRetryWaitMs,
|
|
409
411
|
// Auto-compact between tool calls when the context nears the window
|
|
410
412
|
// limit. The callback is provided by the caller so it can refresh the
|
|
411
413
|
// outer currentChatId; it serializes with the throttle because the
|
package/dist/subagent.js
CHANGED
|
@@ -65,7 +65,7 @@ function subagentPersona(type) {
|
|
|
65
65
|
* returns only the report.
|
|
66
66
|
*/
|
|
67
67
|
export function createSubagentRunner(opts) {
|
|
68
|
-
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries, onAskPermission, } = opts;
|
|
68
|
+
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries, infiniteRetries, maxRetryWaitMs, onAskPermission, } = opts;
|
|
69
69
|
let used = 0;
|
|
70
70
|
return async (req) => {
|
|
71
71
|
if (used >= maxSubagents) {
|
|
@@ -133,6 +133,8 @@ export function createSubagentRunner(opts) {
|
|
|
133
133
|
locale,
|
|
134
134
|
askDeadlineMs,
|
|
135
135
|
maxAfterToolRetries,
|
|
136
|
+
infiniteRetries,
|
|
137
|
+
maxRetryWaitMs,
|
|
136
138
|
// A subagent NEVER touches BACKLOG.md, even in the operator's dev
|
|
137
139
|
// mode: its sub-task is scoped, and a general subagent appending
|
|
138
140
|
// "improvement" notes would be unrelated, unsupervised edits.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.92.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
|