zames_pro 2.95.4 → 2.95.5
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 +19 -0
- package/dist/run-task.js +45 -2
- package/package.json +2 -1
- package/dist/ask-retry.js +0 -10
- package/dist/ask-state.js +0 -80
- package/dist/browser-base.js +0 -69
- package/dist/provider-errors.js +0 -53
- package/dist/retry-policy.js +0 -98
- package/dist/send-slot.js +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.95.5] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **site:** a 404 page, hreflang alternates and cache-busted assets
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **tui:** keep the AskUser question and answer in the log
|
|
19
|
+
- **changelog:** keep a manual Unreleased note inside its group
|
|
20
|
+
- **site:** stop the landing title repeating the site name in og:title
|
|
21
|
+
- **site:** give the social preview its own static image
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **tui:** paint the AskUser question in the tool color
|
|
26
|
+
- drop dead provider-unification modules and stale doc text
|
|
27
|
+
- **site:** a deeper palette, bigger type and calmer cards
|
|
28
|
+
|
|
10
29
|
## [2.95.4] - 2026-11-05
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/dist/run-task.js
CHANGED
|
@@ -2,8 +2,35 @@ import { theme } from './theme.js';
|
|
|
2
2
|
import { withGoal, mergeMessages, isSlashCommand, formatDuration, } from './commands.js';
|
|
3
3
|
import { buildEvaluatorPrompt, buildContinuationDirective, parseGoalVerdict, GOAL_MAX_TURNS, } from './goal.js';
|
|
4
4
|
import { formatCompactTokens, pasteReplacement, expandPastes } from './input.js';
|
|
5
|
-
import { DEFAULT_LOCALE } from './i18n.js';
|
|
5
|
+
import { DEFAULT_LOCALE, translate } from './i18n.js';
|
|
6
6
|
import { providerAskDeadlineMs } from './config.js';
|
|
7
|
+
// The two log lines an `AskUser` turn leaves behind: the question with its
|
|
8
|
+
// options (the recommended one starred) and the operator's answer. PURE, so the
|
|
9
|
+
// wording is unit-tested without a live modal; `runTask` prints them through
|
|
10
|
+
// the editor after the modal closes (the modal's own frame is erased on
|
|
11
|
+
// unmount, and without these lines the question would leave no trace in the
|
|
12
|
+
// terminal or the chat history).
|
|
13
|
+
const ASK_NL = String.fromCharCode(10);
|
|
14
|
+
export function formatAskUserLog(question, options, recommended, answer, t) {
|
|
15
|
+
const qLines = question
|
|
16
|
+
.split(ASK_NL)
|
|
17
|
+
.map((l) => ' ' + l.trim())
|
|
18
|
+
.join(ASK_NL);
|
|
19
|
+
const optLines = options
|
|
20
|
+
.map((o, i) => {
|
|
21
|
+
const mark = i === recommended ? '* ' : '- ';
|
|
22
|
+
const tag = i === recommended ? ' (' + t('askuser.recommended') + ')' : '';
|
|
23
|
+
return ' ' + mark + o + tag;
|
|
24
|
+
})
|
|
25
|
+
.join(ASK_NL);
|
|
26
|
+
return {
|
|
27
|
+
question: '◆ ' + t('askuser.title') + ASK_NL + qLines + ASK_NL + optLines,
|
|
28
|
+
answer: '❯ ' +
|
|
29
|
+
t('askuser.your_answer') +
|
|
30
|
+
' ' +
|
|
31
|
+
(answer === null || answer === '' ? t('askuser.hint_dismiss') : answer),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
7
34
|
// Ask the operator to approve a tool call flagged by an `ask` permission rule
|
|
8
35
|
// While the LineEditor owns the terminal it must be paused first, or the
|
|
9
36
|
// confirmation prompt and the input line fight for the same keys. A non-TTY run
|
|
@@ -429,18 +456,34 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
429
456
|
? async (req) => {
|
|
430
457
|
if (!editor)
|
|
431
458
|
return null;
|
|
459
|
+
// The modal is a SEPARATE Ink app: its frame is erased when it
|
|
460
|
+
// unmounts, so without this the question and the operator's
|
|
461
|
+
// answer leave no trace — neither in the terminal nor in the
|
|
462
|
+
// chat history. Print them as a normal log turn instead.
|
|
463
|
+
const tr = translate(locale);
|
|
432
464
|
editor.pause();
|
|
465
|
+
let answer = null;
|
|
433
466
|
try {
|
|
434
467
|
const { askQuestionInk } = await import('./tui/run-ask.js');
|
|
435
|
-
|
|
468
|
+
answer = await askQuestionInk({
|
|
436
469
|
question: req.question,
|
|
437
470
|
options: req.options,
|
|
438
471
|
recommended: req.recommended,
|
|
439
472
|
allowText: req.allowText,
|
|
440
473
|
});
|
|
474
|
+
return answer;
|
|
441
475
|
}
|
|
442
476
|
finally {
|
|
477
|
+
// Print AFTER resume, never before pause: on the alternate
|
|
478
|
+
// screen the modal unmount wipes everything painted into the
|
|
479
|
+
// old app, so the question and the answer would vanish.
|
|
443
480
|
editor.resume();
|
|
481
|
+
const logged = formatAskUserLog(req.question, req.options, req.recommended, answer, tr);
|
|
482
|
+
// The question block is painted in the TOOL color (the warm
|
|
483
|
+
// one tool calls use) so it reads as a distinct event, not
|
|
484
|
+
// as another grey service line next to the agent's answers.
|
|
485
|
+
editor.printAbove(theme.tool(logged.question));
|
|
486
|
+
editor.printAbove(theme.user(logged.answer));
|
|
444
487
|
}
|
|
445
488
|
}
|
|
446
489
|
: onAskUser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.95.
|
|
3
|
+
"version": "2.95.5",
|
|
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": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"postinstall": "node scripts/postinstall.mjs",
|
|
48
48
|
"prepare": "husky",
|
|
49
49
|
"glm-smoke": "tsx scripts/glm-smoke.mts",
|
|
50
|
+
"glm-warmup": "tsx scripts/glm-warmup.mts",
|
|
50
51
|
"seo:topics": "tsx scripts/seo-topics.mts",
|
|
51
52
|
"ui-smoke": "tsx scripts/ui-smoke.mts"
|
|
52
53
|
},
|
package/dist/ask-retry.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// N1 stage 4: the retry POLICY of a provider's ask() as a PURE function.
|
|
2
|
-
//
|
|
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/ask-state.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// N3: the PHASE DECISION of DeepSeek's `_askOnce()` as a pure state machine.
|
|
2
|
-
//
|
|
3
|
-
// `_askOnce` (src/browser.ts) is the most fragile function of the project: a
|
|
4
|
-
// start-wait loop, a finish loop, the Continue button, the stability window
|
|
5
|
-
// and four detectors (rate limit / context limit / truncated / finished
|
|
6
|
-
// without answer). Until now it was covered only by live smoke runs, and a
|
|
7
|
-
// wrong ordering of the checks kept reappearing (the "Stopped" hang, the
|
|
8
|
-
// "agent stopped with a Continue button", the 15-minute spinner on a dead
|
|
9
|
-
// turn). The Playwright part keeps COLLECTING an observation; the TRANSITION
|
|
10
|
-
// below is pure and unit-tested against the real cases from DESIGN-NOTES.
|
|
11
|
-
/** Grace ticks before a FINISHED-with-answer turn is returned (default 2). */
|
|
12
|
-
export const NET_FINISHED_TICKS = 2;
|
|
13
|
-
/** Ticks a dead (FINISHED, no answer, no Continue) turn waits before giving up. */
|
|
14
|
-
export const DEAD_TURN_TICKS = 6;
|
|
15
|
-
/**
|
|
16
|
-
* One transition of the ask() state machine.
|
|
17
|
-
*
|
|
18
|
-
* ORDER MATTERS and is the whole point of this function:
|
|
19
|
-
* 1. an abort wins over everything (the operator stopped);
|
|
20
|
-
* 2. the CONTEXT limit is checked BEFORE the rate limit — both look like a
|
|
21
|
-
* toast, and retrying an exhausted chat is useless;
|
|
22
|
-
* 3. rate limit / server busy are retryable errors;
|
|
23
|
-
* 4. a truncated turn PREFERS the Continue button (it RESUMES the turn) and
|
|
24
|
-
* only falls back to a resend when there is no button;
|
|
25
|
-
* 5. a PAUSED generation (`continueVisible`) is never "settled": the
|
|
26
|
-
* stability early-return must be checked AFTER it, otherwise a partial
|
|
27
|
-
* answer is returned with Continue still on screen.
|
|
28
|
-
*/
|
|
29
|
-
export function nextPhase(phase, o, opts) {
|
|
30
|
-
if (o.aborted)
|
|
31
|
-
return { kind: 'return', text: 'dom' };
|
|
32
|
-
if (o.contextLimit)
|
|
33
|
-
return { kind: 'throw', error: 'context-limit' };
|
|
34
|
-
if (o.rateLimited)
|
|
35
|
-
return { kind: 'throw', error: 'rate-limit' };
|
|
36
|
-
if (o.serverBusy)
|
|
37
|
-
return { kind: 'throw', error: 'server-busy' };
|
|
38
|
-
// A truncated / answer-less turn: click Continue when possible (it resumes
|
|
39
|
-
// the SAME turn); otherwise resend.
|
|
40
|
-
if (o.truncated || o.noAnswer) {
|
|
41
|
-
if (opts.autoContinue && o.continueVisible) {
|
|
42
|
-
return { kind: 'click-continue' };
|
|
43
|
-
}
|
|
44
|
-
return { kind: 'throw', error: 'incomplete' };
|
|
45
|
-
}
|
|
46
|
-
if (phase === 'start') {
|
|
47
|
-
if (o.isNew)
|
|
48
|
-
return { kind: 'started' };
|
|
49
|
-
if (o.hasText && o.notGenerating && !o.continueVisible) {
|
|
50
|
-
return { kind: 'return', text: 'dom' };
|
|
51
|
-
}
|
|
52
|
-
// A reasoning pause during the THINK phase: click Continue so the answer
|
|
53
|
-
// starts flowing instead of waiting for the start deadline.
|
|
54
|
-
if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
|
|
55
|
-
return { kind: 'click-continue' };
|
|
56
|
-
}
|
|
57
|
-
return { kind: 'wait' };
|
|
58
|
-
}
|
|
59
|
-
// --- finish loop ---------------------------------------------------------
|
|
60
|
-
// The SSE body is ground truth: FINISHED with an answer, after a short grace
|
|
61
|
-
// window, is complete.
|
|
62
|
-
if (o.finishedWithAnswer && opts.netFinishedTicks >= NET_FINISHED_TICKS) {
|
|
63
|
-
return { kind: 'return', text: 'net' };
|
|
64
|
-
}
|
|
65
|
-
// A dead turn (the server ended it with nothing and there is no Continue to
|
|
66
|
-
// resume): give up quickly so ask() resends instead of a long spinner.
|
|
67
|
-
if (opts.deadTicks >= DEAD_TURN_TICKS && !o.continueVisible) {
|
|
68
|
-
return { kind: 'throw', error: 'incomplete' };
|
|
69
|
-
}
|
|
70
|
-
// A PAUSED generation looks stable: check it BEFORE the stability return.
|
|
71
|
-
if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
|
|
72
|
-
return { kind: 'click-continue' };
|
|
73
|
-
}
|
|
74
|
-
if (o.isNew && opts.stableReached) {
|
|
75
|
-
if (o.notGenerating)
|
|
76
|
-
return { kind: 'return', text: 'dom' };
|
|
77
|
-
return { kind: 'wait' };
|
|
78
|
-
}
|
|
79
|
-
return { kind: 'wait' };
|
|
80
|
-
}
|
package/dist/browser-base.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// N1 stage 1: the helpers that were byte-identical in DeepSeekBrowser,
|
|
2
|
-
// QwenBrowser and GlmBrowser. Composition, not inheritance: every provider
|
|
3
|
-
// class owns a page and calls these with it, so the provider-specific logic
|
|
4
|
-
// stays where it is and only the shared mechanics live here.
|
|
5
|
-
/** Best-effort: a page error while marking the tab title must never break a
|
|
6
|
-
* send loop, so every failure is swallowed. */
|
|
7
|
-
export async function setBusyTitle(page, busy, setInDom) {
|
|
8
|
-
try {
|
|
9
|
-
await page.evaluate(setInDom, busy);
|
|
10
|
-
}
|
|
11
|
-
catch { }
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* The first selector that has a VISIBLE element. `timeout` is a per-selector
|
|
15
|
-
* budget, not a total one (the providers rely on that: a login form is tried
|
|
16
|
-
* with a long budget while optional probes use a short one).
|
|
17
|
-
*
|
|
18
|
-
* Two probe styles exist in the providers (`waitFor({state:'visible'})` in
|
|
19
|
-
* DeepSeek/Qwen and `count()` + `isVisible()` in GLM). `style: 'wait'` keeps
|
|
20
|
-
* the first (it also covers a late-mounting element); `style: 'check'` keeps
|
|
21
|
-
* the second (no waiting state, a cheap poll).
|
|
22
|
-
*/
|
|
23
|
-
export async function findVisible(page, selectors, timeout = 1000, style = 'wait') {
|
|
24
|
-
for (const sel of selectors) {
|
|
25
|
-
const loc = page.locator(sel).last();
|
|
26
|
-
if (style === 'wait') {
|
|
27
|
-
try {
|
|
28
|
-
await loc.waitFor({ state: 'visible', timeout });
|
|
29
|
-
return loc;
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
if ((await loc.count()) === 0)
|
|
36
|
-
continue;
|
|
37
|
-
if (await loc.isVisible({ timeout }).catch(() => false))
|
|
38
|
-
return loc;
|
|
39
|
-
}
|
|
40
|
-
catch { }
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Sleep in 100 ms chunks so Esc and the ask() watchdog stay responsive.
|
|
46
|
-
* Returns true when the wait was INTERRUPTED (abort / cancelled ask), so the
|
|
47
|
-
* caller can stop early. `onTick` fires about once a second with the time
|
|
48
|
-
* left — that is what animates the throttle countdown.
|
|
49
|
-
*/
|
|
50
|
-
export async function sleepInterruptible(page, ms, isInterrupted, onTick) {
|
|
51
|
-
const step = 100;
|
|
52
|
-
let left = ms;
|
|
53
|
-
let sinceTick = 0;
|
|
54
|
-
while (left > 0) {
|
|
55
|
-
if (isInterrupted())
|
|
56
|
-
return true;
|
|
57
|
-
const chunk = Math.min(step, left);
|
|
58
|
-
await page.waitForTimeout(chunk);
|
|
59
|
-
left -= chunk;
|
|
60
|
-
if (onTick) {
|
|
61
|
-
sinceTick += chunk;
|
|
62
|
-
if (sinceTick >= 1000) {
|
|
63
|
-
sinceTick = 0;
|
|
64
|
-
onTick(left);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return isInterrupted();
|
|
69
|
-
}
|
package/dist/provider-errors.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// N1 stage 2: the error families shared by the chat providers.
|
|
2
|
-
//
|
|
3
|
-
// Each provider used to declare its own copy (`QwenRateLimitError`,
|
|
4
|
-
// `GlmRateLimitError`, ...), so the retry policy in every `ask()` re-listed the
|
|
5
|
-
// same six families and a new one had to be added three times. The classes
|
|
6
|
-
// here are the SHARED vocabulary; the provider keeps its own pre-check
|
|
7
|
-
// (`instanceof`) only where the semantics genuinely differ (guest limits and
|
|
8
|
-
// the age gate exist on one backend only).
|
|
9
|
-
//
|
|
10
|
-
// `provider` is carried for the operator-facing message only — the retry
|
|
11
|
-
// decision is per CLASS, never per string.
|
|
12
|
-
export class ProviderRateLimitError extends Error {
|
|
13
|
-
constructor(detail) {
|
|
14
|
-
super('rate limit: ' + detail);
|
|
15
|
-
this.name = 'ProviderRateLimitError';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export class ProviderServerBusyError extends Error {
|
|
19
|
-
constructor(detail) {
|
|
20
|
-
super('server busy: ' + detail);
|
|
21
|
-
this.name = 'ProviderServerBusyError';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export class ProviderContextLimitError extends Error {
|
|
25
|
-
constructor(detail) {
|
|
26
|
-
super('context limit: ' + detail);
|
|
27
|
-
this.name = 'ProviderContextLimitError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export class ProviderIncompleteError extends Error {
|
|
31
|
-
constructor(detail) {
|
|
32
|
-
super('generation incomplete: ' + detail);
|
|
33
|
-
this.name = 'ProviderIncompleteError';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export class ProviderGuestLimitError extends Error {
|
|
37
|
-
constructor(detail) {
|
|
38
|
-
super('guest limit: ' + detail);
|
|
39
|
-
this.name = 'ProviderGuestLimitError';
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export class ProviderAgeGateError extends Error {
|
|
43
|
-
constructor(detail) {
|
|
44
|
-
super('age gate: ' + detail);
|
|
45
|
-
this.name = 'ProviderAgeGateError';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export class ProviderCaptchaError extends Error {
|
|
49
|
-
constructor(detail) {
|
|
50
|
-
super('captcha: ' + detail);
|
|
51
|
-
this.name = 'ProviderCaptchaError';
|
|
52
|
-
}
|
|
53
|
-
}
|
package/dist/retry-policy.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
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/send-slot.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// N1 stage 3: the SEND SLOT — the pause before a send that keeps the chat's
|
|
2
|
-
// rate limit happy.
|
|
3
|
-
//
|
|
4
|
-
// Every provider had its own `_waitForSendSlot()` with the same shape (skip a
|
|
5
|
-
// non-agent send, compute the remaining gap, report the animated countdown,
|
|
6
|
-
// mark the state 'paused', sleep interruptibly) and each computed the gap its
|
|
7
|
-
// own way (DeepSeek: interval + thinking bonus; Qwen: interval; GLM: the
|
|
8
|
-
// adaptive sendRuntime). The MECHANICS are shared here; the provider injects
|
|
9
|
-
// the gap and the plumbing.
|
|
10
|
-
/**
|
|
11
|
-
* Wait for the send slot. Returns true when the wait was INTERRUPTED (the
|
|
12
|
-
* caller must then abort the send).
|
|
13
|
-
*/
|
|
14
|
-
export async function waitForSendSlot(agent, deps) {
|
|
15
|
-
if (!agent)
|
|
16
|
-
return false;
|
|
17
|
-
const gap = deps.gapMs();
|
|
18
|
-
if (gap <= 0)
|
|
19
|
-
return false;
|
|
20
|
-
const report = (leftMs) => {
|
|
21
|
-
const secs = Math.max(0, Math.ceil(leftMs / 1000));
|
|
22
|
-
if (deps.onPause) {
|
|
23
|
-
try {
|
|
24
|
-
deps.onPause(secs);
|
|
25
|
-
}
|
|
26
|
-
catch { }
|
|
27
|
-
}
|
|
28
|
-
else if (deps.onPauseText) {
|
|
29
|
-
deps.onPauseText(secs);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
report(gap);
|
|
33
|
-
if (deps.emitState)
|
|
34
|
-
deps.emitState('paused');
|
|
35
|
-
return await deps.sleep(gap, report);
|
|
36
|
-
}
|