sequant 2.9.0 → 2.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bin/cli.js +143 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +87 -0
- package/dist/src/lib/settings.js +106 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The renderer is event-driven: a `ProgressEvent` flows in and the renderer
|
|
5
5
|
* decides whether to update the live zone, append an event line, or both.
|
|
6
6
|
*/
|
|
7
|
-
export type ProgressEventKind = "start" | "complete" | "failed";
|
|
7
|
+
export type ProgressEventKind = "start" | "complete" | "failed" | "waiting";
|
|
8
8
|
/** Raw event from batch-executor `emitProgressLine` / `onProgress` callbacks. */
|
|
9
9
|
export interface ProgressEvent {
|
|
10
10
|
issue: number;
|
|
@@ -19,15 +19,34 @@ export interface ProgressEvent {
|
|
|
19
19
|
* cell as `loop N/M` (#624 Item 3).
|
|
20
20
|
*/
|
|
21
21
|
iteration?: number;
|
|
22
|
+
/**
|
|
23
|
+
* `"waiting"` (#804): display message for an in-progress auto-wait.
|
|
24
|
+
*/
|
|
25
|
+
text?: string;
|
|
26
|
+
/**
|
|
27
|
+
* `"waiting"` (#804): epoch ms at which the auto-wait ends. Absent on the
|
|
28
|
+
* terminal notice, which returns the phase to `running`.
|
|
29
|
+
*/
|
|
30
|
+
wakeAtMs?: number;
|
|
22
31
|
}
|
|
23
32
|
/** Per-phase status tracked inside the renderer state machine. */
|
|
24
33
|
export interface PhaseState {
|
|
25
34
|
name: string;
|
|
26
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `"waiting"` (#804) is a *paused* running phase, not a terminal state: the
|
|
37
|
+
* phase spawn has failed on an exhausted rate-limit window and the executor
|
|
38
|
+
* is sleeping until it reopens. It returns to `"running"` on wake.
|
|
39
|
+
*/
|
|
40
|
+
status: "pending" | "running" | "done" | "failed" | "waiting";
|
|
27
41
|
startedAt?: number;
|
|
28
42
|
durationMs?: number;
|
|
29
43
|
/** Loop iteration label (e.g. "loop 2/3"). */
|
|
30
44
|
loopIteration?: number;
|
|
45
|
+
/**
|
|
46
|
+
* #804: epoch ms at which an in-progress auto-wait ends. Set only while
|
|
47
|
+
* `status === "waiting"`; cleared on resume.
|
|
48
|
+
*/
|
|
49
|
+
wakeAtMs?: number;
|
|
31
50
|
/**
|
|
32
51
|
* #624 Item 4: normalized signature of the most recent failure for THIS
|
|
33
52
|
* phase (ANSI-stripped, lowercased, first 80 chars, trimmed). Per-phase so
|
|
@@ -98,6 +117,8 @@ export interface IssueSummary {
|
|
|
98
117
|
loopTriggered?: boolean;
|
|
99
118
|
prNumber?: number;
|
|
100
119
|
prUrl?: string;
|
|
120
|
+
/** Set when PR creation was skipped for lack of commits ahead of base (#920). */
|
|
121
|
+
prSkippedReason?: string;
|
|
101
122
|
failureReason?: string;
|
|
102
123
|
qaVerdict?: string;
|
|
103
124
|
unmetCount?: number;
|
|
@@ -191,11 +212,6 @@ export interface RenderOptions {
|
|
|
191
212
|
* tracks the configured maximum instead of being hardcoded. Defaults to 3.
|
|
192
213
|
*/
|
|
193
214
|
maxLoopIterations?: number;
|
|
194
|
-
/**
|
|
195
|
-
* When true, `renderSummary` is rendered even if no issues were registered.
|
|
196
|
-
* Default: false (matches existing displaySummary behaviour).
|
|
197
|
-
*/
|
|
198
|
-
alwaysRenderSummary?: boolean;
|
|
199
215
|
/**
|
|
200
216
|
* #647: inject a `log-update` instance (typically built via
|
|
201
217
|
* `createLogUpdate(stream)` against a custom stream). Used by the
|
|
@@ -88,6 +88,8 @@ export declare class NonTTYRenderer extends BaseRenderer {
|
|
|
88
88
|
private readonly columnsOverride?;
|
|
89
89
|
private readonly maxLoopIterations;
|
|
90
90
|
private lastEventAt;
|
|
91
|
+
/** #860 AC-6: `issue:phase` keys whose auto-wait start line was emitted. */
|
|
92
|
+
private readonly announcedWaits;
|
|
91
93
|
constructor(options: RenderOptions);
|
|
92
94
|
private getColumns;
|
|
93
95
|
private startHeartbeat;
|
|
@@ -95,6 +97,14 @@ export declare class NonTTYRenderer extends BaseRenderer {
|
|
|
95
97
|
tickHeartbeatNow(): void;
|
|
96
98
|
private tickHeartbeat;
|
|
97
99
|
protected afterEvent(event: ProgressEvent, state: IssueState): void;
|
|
100
|
+
/**
|
|
101
|
+
* Announce auto-wait transitions (#860 AC-6). Only the first notice of a
|
|
102
|
+
* wait and the terminal notice that ends it produce lines — the ~15s ticks
|
|
103
|
+
* in between are absorbed here (the periodic signal is `tickHeartbeat`).
|
|
104
|
+
* Before this, waiting events fell through `emitEventLine`'s failure branch
|
|
105
|
+
* and printed a spurious `✘` line per tick.
|
|
106
|
+
*/
|
|
107
|
+
private emitWaitTransitionLine;
|
|
98
108
|
private emitEventLine;
|
|
99
109
|
/** Append a single `\n`-terminated line with `[HH:MM:SS]` prefix. */
|
|
100
110
|
private emitLine;
|
|
@@ -19,6 +19,7 @@ import chalk from "chalk";
|
|
|
19
19
|
import logUpdate from "log-update";
|
|
20
20
|
import stringWidth from "string-width";
|
|
21
21
|
import { formatElapsedTime, formatTimestamp } from "./format.js";
|
|
22
|
+
import { formatResetTime } from "../errors.js";
|
|
22
23
|
import { pipelineHasFailed } from "../workflow/status-derivation.js";
|
|
23
24
|
const DEFAULT_LIVE_TICK_MS = 1000;
|
|
24
25
|
const DEFAULT_NON_TTY_HEARTBEAT_MS = 60_000;
|
|
@@ -229,6 +230,23 @@ class BaseRenderer {
|
|
|
229
230
|
state.subStatus = undefined;
|
|
230
231
|
return;
|
|
231
232
|
}
|
|
233
|
+
// #804 AC-7: an auto-wait pauses a phase rather than ending it. Presence of
|
|
234
|
+
// `wakeAtMs` distinguishes an in-progress wait from the terminal notice
|
|
235
|
+
// that resumes it, so the live zone never strands a "waiting" cell.
|
|
236
|
+
if (event.event === "waiting") {
|
|
237
|
+
if (event.wakeAtMs !== undefined) {
|
|
238
|
+
phase.status = "waiting";
|
|
239
|
+
phase.wakeAtMs = event.wakeAtMs;
|
|
240
|
+
state.subStatus = event.text;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (phase.status === "waiting")
|
|
244
|
+
phase.status = "running";
|
|
245
|
+
phase.wakeAtMs = undefined;
|
|
246
|
+
state.subStatus = undefined;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
232
250
|
if (event.event === "complete") {
|
|
233
251
|
phase.status = "done";
|
|
234
252
|
if (event.durationSeconds !== undefined) {
|
|
@@ -329,6 +347,8 @@ export class NonTTYRenderer extends BaseRenderer {
|
|
|
329
347
|
columnsOverride;
|
|
330
348
|
maxLoopIterations;
|
|
331
349
|
lastEventAt;
|
|
350
|
+
/** #860 AC-6: `issue:phase` keys whose auto-wait start line was emitted. */
|
|
351
|
+
announcedWaits = new Set();
|
|
332
352
|
constructor(options) {
|
|
333
353
|
super(options);
|
|
334
354
|
this.heartbeatMs =
|
|
@@ -367,15 +387,53 @@ export class NonTTYRenderer extends BaseRenderer {
|
|
|
367
387
|
return;
|
|
368
388
|
const parts = running.map((s) => {
|
|
369
389
|
const elapsedSec = s.startedAt !== undefined ? (this.now() - s.startedAt) / 1000 : 0;
|
|
390
|
+
// #860 AC-6: a phase paused on an auto-wait names its wake time instead
|
|
391
|
+
// of masquerading as ordinary running time — this heartbeat is the only
|
|
392
|
+
// periodic signal a non-TTY (background) run gets during a multi-hour
|
|
393
|
+
// wait, and without the wake time it is indistinguishable from a hang.
|
|
394
|
+
const waiting = s.phases.find((p) => p.name === s.currentPhase && p.status === "waiting");
|
|
395
|
+
if (waiting?.wakeAtMs !== undefined) {
|
|
396
|
+
const leftSec = Math.max(0, (waiting.wakeAtMs - this.now()) / 1000);
|
|
397
|
+
return `#${s.issueNumber} ${s.currentPhase} ⏸ rate-limit window — resuming at ${formatResetTime(waiting.wakeAtMs)} (${formatElapsedTime(leftSec)} left)`;
|
|
398
|
+
}
|
|
370
399
|
return `#${s.issueNumber} ${s.currentPhase} (${formatElapsedTime(elapsedSec)})`;
|
|
371
400
|
});
|
|
372
401
|
this.emitLine(`⏱ still running: ${parts.join(", ")}`);
|
|
373
402
|
}
|
|
374
403
|
afterEvent(event, state) {
|
|
375
404
|
super.afterEvent(event, state);
|
|
405
|
+
// #860 AC-6: waiting notices are liveness, not progress. The wait emits a
|
|
406
|
+
// tick every ~15s, so counting them as events would hold `lastEventAt`
|
|
407
|
+
// fresh forever and silence the heartbeat for the entire multi-hour wait.
|
|
408
|
+
if (event.event === "waiting") {
|
|
409
|
+
this.emitWaitTransitionLine(event);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
376
412
|
this.lastEventAt = this.now();
|
|
377
413
|
this.emitEventLine(event, state);
|
|
378
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Announce auto-wait transitions (#860 AC-6). Only the first notice of a
|
|
417
|
+
* wait and the terminal notice that ends it produce lines — the ~15s ticks
|
|
418
|
+
* in between are absorbed here (the periodic signal is `tickHeartbeat`).
|
|
419
|
+
* Before this, waiting events fell through `emitEventLine`'s failure branch
|
|
420
|
+
* and printed a spurious `✘` line per tick.
|
|
421
|
+
*/
|
|
422
|
+
emitWaitTransitionLine(event) {
|
|
423
|
+
const key = `${event.issue}:${event.phase}`;
|
|
424
|
+
const c = colorize(this.noColor);
|
|
425
|
+
if (event.wakeAtMs !== undefined) {
|
|
426
|
+
if (this.announcedWaits.has(key))
|
|
427
|
+
return;
|
|
428
|
+
this.announcedWaits.add(key);
|
|
429
|
+
this.emitLine(`${c.yellow("⏸")} #${event.issue} ${event.phase} ${event.text ?? `rate-limit window — resuming at ${formatResetTime(event.wakeAtMs)}`}`);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
if (!this.announcedWaits.delete(key))
|
|
433
|
+
return;
|
|
434
|
+
this.emitLine(`${c.cyan("▸")} #${event.issue} ${event.phase} ${event.text ?? "auto-wait complete — resuming"}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
379
437
|
emitEventLine(event, state) {
|
|
380
438
|
const c = colorize(this.noColor);
|
|
381
439
|
const phase = state.phases.find((p) => p.name === event.phase);
|
|
@@ -1135,6 +1193,10 @@ export class TTYRenderer extends BaseRenderer {
|
|
|
1135
1193
|
return c.green(`${p.name} ✔${p.durationMs ? ` ${formatElapsedTime(p.durationMs / 1000)}` : ""}`);
|
|
1136
1194
|
if (p.status === "failed")
|
|
1137
1195
|
return c.red(`${p.name} ✘`);
|
|
1196
|
+
// #804: a paused phase reads as `qa waiting 14:33`, so the wake time
|
|
1197
|
+
// is visible in the live zone itself, not only the sub-status line.
|
|
1198
|
+
if (p.status === "waiting")
|
|
1199
|
+
return c.yellow(`${p.name} waiting${p.wakeAtMs ? ` ${formatResetTime(p.wakeAtMs)}` : ""}`);
|
|
1138
1200
|
if (p.status === "running")
|
|
1139
1201
|
return c.cyan(`${p.name} running`);
|
|
1140
1202
|
// #672 AC-3: pending cells render as `name –` (en dash) so the live
|
|
@@ -1270,7 +1332,13 @@ function renderSummaryDetail(r, ctx) {
|
|
|
1270
1332
|
.map((p) => (p.success ? c.green(p.name) : c.red(p.name)))
|
|
1271
1333
|
.join(" → ");
|
|
1272
1334
|
const pr = r.prNumber ? ` · PR #${r.prNumber}` : "";
|
|
1273
|
-
|
|
1335
|
+
// #920: a phase-restricted run (no PR, no failure) still needs the reason
|
|
1336
|
+
// stated — otherwise a passing spec-only run looks indistinguishable from
|
|
1337
|
+
// one where PR creation was simply never attempted for no stated reason.
|
|
1338
|
+
const extras = !r.prNumber && r.prSkippedReason
|
|
1339
|
+
? [c.dim(`PR skipped — ${r.prSkippedReason}`)]
|
|
1340
|
+
: [];
|
|
1341
|
+
return { summary: `${phaseSeq}${pr}`, extras };
|
|
1274
1342
|
}
|
|
1275
1343
|
// Failed → multi-line detail.
|
|
1276
1344
|
const reason = r.failureReason ?? "failure";
|
package/dist/src/lib/errors.d.ts
CHANGED
|
@@ -145,15 +145,65 @@ export interface RateLimitInfoLike {
|
|
|
145
145
|
hasChargeableSavedPaymentMethod?: boolean;
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
-
* True when the rate-limit info
|
|
149
|
-
*
|
|
148
|
+
* True when the rate-limit info describes an exhausted *window* that will
|
|
149
|
+
* reopen at a known future time: a recognized window `rateLimitType` plus a
|
|
150
|
+
* `resetsAt` still in the future (#860 AC-1).
|
|
151
|
+
*
|
|
152
|
+
* This is the discriminator between "subscription window closed until 07:00"
|
|
153
|
+
* and "account needs credits". The fields that would answer that directly
|
|
154
|
+
* (`canUserPurchaseCredits`, `hasChargeableSavedPaymentMethod`) are absent
|
|
155
|
+
* from every real captured payload, so the window shape is the proxy — and
|
|
156
|
+
* any unrecognized shape returns false (fail closed, #860 AC-3).
|
|
157
|
+
*
|
|
158
|
+
* Explicitly informational statuses (`allowed` / `allowed_warning`) are never
|
|
159
|
+
* waitable. The driver retains marker-carrying warnings as failure-grade
|
|
160
|
+
* (pre-#732 semantics, unchanged), so a stale "you are nearing your limit"
|
|
161
|
+
* warning can be misattributed to an unrelated phase failure — classifying it
|
|
162
|
+
* waitable would upgrade that misattribution from a cheap immediate halt to a
|
|
163
|
+
* multi-hour sleep. The 26 real captures carry no `status` field at all, so
|
|
164
|
+
* absent status stays waitable; only a status that affirmatively says
|
|
165
|
+
* "not a rejection" is excluded.
|
|
166
|
+
*/
|
|
167
|
+
export declare function isWaitableWindow(info: RateLimitInfoLike, now?: number): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Vocabulary-drift telemetry for the #860 fail-closed path: returns a hint
|
|
170
|
+
* string when a payload was terminal ONLY because its `rateLimitType` is not
|
|
171
|
+
* in the recognized window allowlist — i.e. it carries `out_of_credits` plus a
|
|
172
|
+
* live future `resetsAt` and a *present but unrecognized* window type, and no
|
|
173
|
+
* explicit `credits_required`. If Anthropic renames or adds a window type,
|
|
174
|
+
* the halt message names the rejected type instead of silently reading as an
|
|
175
|
+
* ordinary wallet failure. A missing `rateLimitType` is NOT drift evidence
|
|
176
|
+
* (API-account payloads may legitimately omit it), so no hint fires there.
|
|
177
|
+
*/
|
|
178
|
+
export declare function unrecognizedWindowHint(info: RateLimitInfoLike, now?: number): string | null;
|
|
179
|
+
/**
|
|
180
|
+
* True when the rate-limit info represents a billing/credits failure (which
|
|
181
|
+
* waiting cannot fix), rather than a transient throttle or an exhausted
|
|
182
|
+
* window.
|
|
183
|
+
*
|
|
184
|
+
* Narrowed by #860: a subscription plan hitting its five-hour cap with
|
|
185
|
+
* overage disabled emits `overageDisabledReason: "out_of_credits"` *plus* a
|
|
186
|
+
* window type and a live `resetsAt` — a pause, not a wallet failure. That
|
|
187
|
+
* shape is excluded here so it classifies as a retryable {@link RateLimitError}
|
|
188
|
+
* and `--auto-wait` (#804) can act on it. An explicit
|
|
189
|
+
* `errorCode: "credits_required"` stays terminal even alongside window
|
|
190
|
+
* evidence — it is the SDK's direct "purchase needed" signal (#860 AC-2).
|
|
191
|
+
* Anything short of the full recognized window shape remains terminal
|
|
192
|
+
* (fail closed, #860 AC-3).
|
|
150
193
|
*/
|
|
151
|
-
export declare function isBillingFailure(info: RateLimitInfoLike): boolean;
|
|
194
|
+
export declare function isBillingFailure(info: RateLimitInfoLike, now?: number): boolean;
|
|
152
195
|
/**
|
|
153
196
|
* True when the rate-limit info represents an actual failure (rejection or
|
|
154
197
|
* billing), as opposed to an informational `allowed` / `allowed_warning`
|
|
155
198
|
* event. The driver uses this to avoid mis-attributing a stale warning event
|
|
156
199
|
* to an unrelated phase failure.
|
|
200
|
+
*
|
|
201
|
+
* Built on the raw billing *markers*, not the #860-narrowed classification:
|
|
202
|
+
* the captured five-hour payloads cannot prove they carried
|
|
203
|
+
* `status: "rejected"`, so narrowing here could silently drop the very events
|
|
204
|
+
* #860 exists to keep (they'd fall back to a metadata-less assistant error and
|
|
205
|
+
* auto-wait would stay inert). Retention semantics are unchanged; only the
|
|
206
|
+
* billing-vs-waitable *classification* narrowed.
|
|
157
207
|
*/
|
|
158
208
|
export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean;
|
|
159
209
|
/**
|
|
@@ -162,6 +212,20 @@ export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean
|
|
|
162
212
|
* displayed: values below ~1e12 are seconds, otherwise milliseconds.
|
|
163
213
|
*/
|
|
164
214
|
export declare function resetsAtToMs(resetsAt: number): number;
|
|
215
|
+
/**
|
|
216
|
+
* Format a Unix timestamp (seconds or ms) as a local time string.
|
|
217
|
+
*
|
|
218
|
+
* Bare `HH:MM` when the reset falls on the current local calendar day;
|
|
219
|
+
* date-qualified `MM-DD HH:MM` otherwise. Also used for #804's auto-wait wake
|
|
220
|
+
* time (a wake is `resetsAt + buffer`, already in ms, which `resetsAtToMs`
|
|
221
|
+
* passes through unchanged) so both render in one convention.
|
|
222
|
+
*
|
|
223
|
+
* Multi-day windows
|
|
224
|
+
* (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
|
|
225
|
+
* as "later today" and misleads the user (#732 QA follow-up), so the date is
|
|
226
|
+
* included whenever the reset is not today.
|
|
227
|
+
*/
|
|
228
|
+
export declare function formatResetTime(resetsAt: number): string;
|
|
165
229
|
/**
|
|
166
230
|
* Build a user-facing message from rate-limit info, naming the real cause:
|
|
167
231
|
* - billing/credits → "Out of credits" (enriched with purchasable vs hard
|
|
@@ -169,14 +233,22 @@ export declare function resetsAtToMs(resetsAt: number): number;
|
|
|
169
233
|
* - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
|
|
170
234
|
* "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
|
|
171
235
|
* `resetsAt` is absent)
|
|
236
|
+
*
|
|
237
|
+
* `now` feeds the #860 waitable-window classification so message and error
|
|
238
|
+
* type are derived against the same instant (and tests can pin the clock).
|
|
239
|
+
* A waitable window renders through the rate-limited branch — its reset time
|
|
240
|
+
* is the actionable fact; "Out of credits" would misname a pause as a wallet
|
|
241
|
+
* failure.
|
|
172
242
|
*/
|
|
173
|
-
export declare function formatRateLimitMessage(info: RateLimitInfoLike): string;
|
|
243
|
+
export declare function formatRateLimitMessage(info: RateLimitInfoLike, now?: number): string;
|
|
174
244
|
/**
|
|
175
245
|
* Construct the appropriate typed error from structured rate-limit info.
|
|
176
246
|
* Billing/credits failures become a non-retryable {@link BillingError};
|
|
177
|
-
* transient throttles
|
|
247
|
+
* transient throttles AND exhausted-but-reopening windows (#860) become a
|
|
248
|
+
* retryable {@link RateLimitError}. `now` pins the waitable-window check to
|
|
249
|
+
* one instant across message and classification.
|
|
178
250
|
*/
|
|
179
|
-
export declare function createRateLimitError(info: RateLimitInfoLike): RateLimitError | BillingError;
|
|
251
|
+
export declare function createRateLimitError(info: RateLimitInfoLike, now?: number): RateLimitError | BillingError;
|
|
180
252
|
/**
|
|
181
253
|
* Map of error type names to their constructors.
|
|
182
254
|
* Used for deserialization from logs.
|
package/dist/src/lib/errors.js
CHANGED
|
@@ -107,21 +107,122 @@ export class BillingError extends SequantError {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* Recognized rate-limit *window* vocabulary — limit types whose exhaustion is
|
|
111
|
+
* a pause that reopens at `resetsAt`, not a wallet state. `five_hour` is the
|
|
112
|
+
* subscription session window; `seven_day` is prefix-matched because the SDK
|
|
113
|
+
* emits model-qualified variants (`seven_day*`, see {@link formatResetTime}).
|
|
114
|
+
*
|
|
115
|
+
* Deliberately an allowlist (#860 AC-3): this encodes an inference about
|
|
116
|
+
* Anthropic's payload vocabulary, so an unlisted type (e.g. `overage`) must
|
|
117
|
+
* fail closed to the terminal billing path rather than into a five-hour sleep.
|
|
118
|
+
*/
|
|
119
|
+
const WAITABLE_WINDOW_TYPE_RE = /^(five_hour|seven_day)/;
|
|
120
|
+
/**
|
|
121
|
+
* True when the rate-limit info describes an exhausted *window* that will
|
|
122
|
+
* reopen at a known future time: a recognized window `rateLimitType` plus a
|
|
123
|
+
* `resetsAt` still in the future (#860 AC-1).
|
|
124
|
+
*
|
|
125
|
+
* This is the discriminator between "subscription window closed until 07:00"
|
|
126
|
+
* and "account needs credits". The fields that would answer that directly
|
|
127
|
+
* (`canUserPurchaseCredits`, `hasChargeableSavedPaymentMethod`) are absent
|
|
128
|
+
* from every real captured payload, so the window shape is the proxy — and
|
|
129
|
+
* any unrecognized shape returns false (fail closed, #860 AC-3).
|
|
130
|
+
*
|
|
131
|
+
* Explicitly informational statuses (`allowed` / `allowed_warning`) are never
|
|
132
|
+
* waitable. The driver retains marker-carrying warnings as failure-grade
|
|
133
|
+
* (pre-#732 semantics, unchanged), so a stale "you are nearing your limit"
|
|
134
|
+
* warning can be misattributed to an unrelated phase failure — classifying it
|
|
135
|
+
* waitable would upgrade that misattribution from a cheap immediate halt to a
|
|
136
|
+
* multi-hour sleep. The 26 real captures carry no `status` field at all, so
|
|
137
|
+
* absent status stays waitable; only a status that affirmatively says
|
|
138
|
+
* "not a rejection" is excluded.
|
|
139
|
+
*/
|
|
140
|
+
export function isWaitableWindow(info, now = Date.now()) {
|
|
141
|
+
if (info.status === "allowed" || info.status === "allowed_warning") {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (typeof info.rateLimitType !== "string")
|
|
145
|
+
return false;
|
|
146
|
+
if (!WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
|
|
147
|
+
return false;
|
|
148
|
+
if (typeof info.resetsAt !== "number")
|
|
149
|
+
return false;
|
|
150
|
+
return resetsAtToMs(info.resetsAt) > now;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Vocabulary-drift telemetry for the #860 fail-closed path: returns a hint
|
|
154
|
+
* string when a payload was terminal ONLY because its `rateLimitType` is not
|
|
155
|
+
* in the recognized window allowlist — i.e. it carries `out_of_credits` plus a
|
|
156
|
+
* live future `resetsAt` and a *present but unrecognized* window type, and no
|
|
157
|
+
* explicit `credits_required`. If Anthropic renames or adds a window type,
|
|
158
|
+
* the halt message names the rejected type instead of silently reading as an
|
|
159
|
+
* ordinary wallet failure. A missing `rateLimitType` is NOT drift evidence
|
|
160
|
+
* (API-account payloads may legitimately omit it), so no hint fires there.
|
|
161
|
+
*/
|
|
162
|
+
export function unrecognizedWindowHint(info, now = Date.now()) {
|
|
163
|
+
if (info.errorCode === "credits_required")
|
|
164
|
+
return null;
|
|
165
|
+
if (info.overageDisabledReason !== "out_of_credits")
|
|
166
|
+
return null;
|
|
167
|
+
if (info.status === "allowed" || info.status === "allowed_warning") {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (typeof info.rateLimitType !== "string")
|
|
171
|
+
return null;
|
|
172
|
+
if (WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
|
|
173
|
+
return null;
|
|
174
|
+
if (typeof info.resetsAt !== "number")
|
|
175
|
+
return null;
|
|
176
|
+
if (resetsAtToMs(info.resetsAt) <= now)
|
|
177
|
+
return null;
|
|
178
|
+
return `unrecognized window type "${info.rateLimitType}" with a future reset — treated as terminal (auto-wait recognizes five_hour/seven_day)`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* True when the info carries an explicit billing/credits marker, regardless
|
|
182
|
+
* of whether a live window would make it waitable. This is the pre-#860
|
|
183
|
+
* `isBillingFailure` predicate, kept for failure-*detection* sites
|
|
184
|
+
* ({@link isRateLimitFailureInfo}) whose retention semantics must not narrow.
|
|
112
185
|
*/
|
|
113
|
-
|
|
186
|
+
function hasBillingMarkers(info) {
|
|
114
187
|
return (info.errorCode === "credits_required" ||
|
|
115
188
|
info.overageDisabledReason === "out_of_credits");
|
|
116
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* True when the rate-limit info represents a billing/credits failure (which
|
|
192
|
+
* waiting cannot fix), rather than a transient throttle or an exhausted
|
|
193
|
+
* window.
|
|
194
|
+
*
|
|
195
|
+
* Narrowed by #860: a subscription plan hitting its five-hour cap with
|
|
196
|
+
* overage disabled emits `overageDisabledReason: "out_of_credits"` *plus* a
|
|
197
|
+
* window type and a live `resetsAt` — a pause, not a wallet failure. That
|
|
198
|
+
* shape is excluded here so it classifies as a retryable {@link RateLimitError}
|
|
199
|
+
* and `--auto-wait` (#804) can act on it. An explicit
|
|
200
|
+
* `errorCode: "credits_required"` stays terminal even alongside window
|
|
201
|
+
* evidence — it is the SDK's direct "purchase needed" signal (#860 AC-2).
|
|
202
|
+
* Anything short of the full recognized window shape remains terminal
|
|
203
|
+
* (fail closed, #860 AC-3).
|
|
204
|
+
*/
|
|
205
|
+
export function isBillingFailure(info, now = Date.now()) {
|
|
206
|
+
if (info.errorCode === "credits_required")
|
|
207
|
+
return true;
|
|
208
|
+
return (info.overageDisabledReason === "out_of_credits" &&
|
|
209
|
+
!isWaitableWindow(info, now));
|
|
210
|
+
}
|
|
117
211
|
/**
|
|
118
212
|
* True when the rate-limit info represents an actual failure (rejection or
|
|
119
213
|
* billing), as opposed to an informational `allowed` / `allowed_warning`
|
|
120
214
|
* event. The driver uses this to avoid mis-attributing a stale warning event
|
|
121
215
|
* to an unrelated phase failure.
|
|
216
|
+
*
|
|
217
|
+
* Built on the raw billing *markers*, not the #860-narrowed classification:
|
|
218
|
+
* the captured five-hour payloads cannot prove they carried
|
|
219
|
+
* `status: "rejected"`, so narrowing here could silently drop the very events
|
|
220
|
+
* #860 exists to keep (they'd fall back to a metadata-less assistant error and
|
|
221
|
+
* auto-wait would stay inert). Retention semantics are unchanged; only the
|
|
222
|
+
* billing-vs-waitable *classification* narrowed.
|
|
122
223
|
*/
|
|
123
224
|
export function isRateLimitFailureInfo(info) {
|
|
124
|
-
return info.status === "rejected" ||
|
|
225
|
+
return info.status === "rejected" || hasBillingMarkers(info);
|
|
125
226
|
}
|
|
126
227
|
/**
|
|
127
228
|
* Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
|
|
@@ -135,12 +236,16 @@ export function resetsAtToMs(resetsAt) {
|
|
|
135
236
|
* Format a Unix timestamp (seconds or ms) as a local time string.
|
|
136
237
|
*
|
|
137
238
|
* Bare `HH:MM` when the reset falls on the current local calendar day;
|
|
138
|
-
* date-qualified `MM-DD HH:MM` otherwise.
|
|
239
|
+
* date-qualified `MM-DD HH:MM` otherwise. Also used for #804's auto-wait wake
|
|
240
|
+
* time (a wake is `resetsAt + buffer`, already in ms, which `resetsAtToMs`
|
|
241
|
+
* passes through unchanged) so both render in one convention.
|
|
242
|
+
*
|
|
243
|
+
* Multi-day windows
|
|
139
244
|
* (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
|
|
140
245
|
* as "later today" and misleads the user (#732 QA follow-up), so the date is
|
|
141
246
|
* included whenever the reset is not today.
|
|
142
247
|
*/
|
|
143
|
-
function formatResetTime(resetsAt) {
|
|
248
|
+
export function formatResetTime(resetsAt) {
|
|
144
249
|
const ms = resetsAtToMs(resetsAt);
|
|
145
250
|
const d = new Date(ms);
|
|
146
251
|
const hh = String(d.getHours()).padStart(2, "0");
|
|
@@ -163,16 +268,27 @@ function formatResetTime(resetsAt) {
|
|
|
163
268
|
* - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
|
|
164
269
|
* "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
|
|
165
270
|
* `resetsAt` is absent)
|
|
271
|
+
*
|
|
272
|
+
* `now` feeds the #860 waitable-window classification so message and error
|
|
273
|
+
* type are derived against the same instant (and tests can pin the clock).
|
|
274
|
+
* A waitable window renders through the rate-limited branch — its reset time
|
|
275
|
+
* is the actionable fact; "Out of credits" would misname a pause as a wallet
|
|
276
|
+
* failure.
|
|
166
277
|
*/
|
|
167
|
-
export function formatRateLimitMessage(info) {
|
|
168
|
-
if (isBillingFailure(info)) {
|
|
278
|
+
export function formatRateLimitMessage(info, now = Date.now()) {
|
|
279
|
+
if (isBillingFailure(info, now)) {
|
|
280
|
+
// #860 drift telemetry: when the ONLY reason this is terminal is an
|
|
281
|
+
// unrecognized window type, say so — the message is the one channel that
|
|
282
|
+
// reaches run output, logs, and `PhaseResult.error` on every display path.
|
|
283
|
+
const hint = unrecognizedWindowHint(info, now);
|
|
284
|
+
const suffix = hint ? ` (${hint})` : "";
|
|
169
285
|
if (info.canUserPurchaseCredits === true) {
|
|
170
|
-
return
|
|
286
|
+
return `Out of credits — purchasable${suffix}`;
|
|
171
287
|
}
|
|
172
288
|
if (info.canUserPurchaseCredits === false) {
|
|
173
|
-
return
|
|
289
|
+
return `Out of credits — hard limit${suffix}`;
|
|
174
290
|
}
|
|
175
|
-
return
|
|
291
|
+
return `Out of credits${suffix}`;
|
|
176
292
|
}
|
|
177
293
|
if (info.resetsAt !== undefined) {
|
|
178
294
|
return `Rate limited — resets at ${formatResetTime(info.resetsAt)}`;
|
|
@@ -182,10 +298,12 @@ export function formatRateLimitMessage(info) {
|
|
|
182
298
|
/**
|
|
183
299
|
* Construct the appropriate typed error from structured rate-limit info.
|
|
184
300
|
* Billing/credits failures become a non-retryable {@link BillingError};
|
|
185
|
-
* transient throttles
|
|
301
|
+
* transient throttles AND exhausted-but-reopening windows (#860) become a
|
|
302
|
+
* retryable {@link RateLimitError}. `now` pins the waitable-window check to
|
|
303
|
+
* one instant across message and classification.
|
|
186
304
|
*/
|
|
187
|
-
export function createRateLimitError(info) {
|
|
188
|
-
const message = formatRateLimitMessage(info);
|
|
305
|
+
export function createRateLimitError(info, now = Date.now()) {
|
|
306
|
+
const message = formatRateLimitMessage(info, now);
|
|
189
307
|
const metadata = {
|
|
190
308
|
resetsAt: info.resetsAt,
|
|
191
309
|
rateLimitType: info.rateLimitType,
|
|
@@ -194,7 +312,7 @@ export function createRateLimitError(info) {
|
|
|
194
312
|
canUserPurchaseCredits: info.canUserPurchaseCredits,
|
|
195
313
|
hasChargeableSavedPaymentMethod: info.hasChargeableSavedPaymentMethod,
|
|
196
314
|
};
|
|
197
|
-
return isBillingFailure(info)
|
|
315
|
+
return isBillingFailure(info, now)
|
|
198
316
|
? new BillingError(message, metadata)
|
|
199
317
|
: new RateLimitError(message, metadata);
|
|
200
318
|
}
|