gsd-pi 2.71.0-dev.977c553 → 2.71.0-dev.e17e0ce
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/dist/cli.js +12 -3
- package/dist/headless-events.d.ts +2 -0
- package/dist/headless-events.js +7 -0
- package/dist/headless.js +16 -3
- package/dist/mcp-server.js +6 -6
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +139 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -4
- package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto/session.js +11 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
- package/dist/resources/extensions/gsd/auto-model-selection.js +10 -2
- package/dist/resources/extensions/gsd/auto-start.js +13 -6
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +52 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +66 -51
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
- package/dist/resources/extensions/gsd/commands/handlers/core.js +56 -11
- package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/forensics.js +19 -6
- package/dist/resources/extensions/gsd/guided-flow.js +5 -10
- package/dist/resources/extensions/gsd/metrics.js +1 -0
- package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
- package/dist/resources/extensions/gsd/notification-overlay.js +20 -5
- package/dist/resources/extensions/gsd/notification-store.js +51 -1
- package/dist/resources/extensions/gsd/notification-widget.js +5 -13
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/dist/resources/extensions/gsd/session-model-override.js +25 -0
- package/dist/resources/extensions/gsd/shortcut-defs.js +34 -0
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +21 -11
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +31 -11
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +7 -4
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/anthropic.ts +8 -4
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js +64 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +22 -18
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +24 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +43 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
- package/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +78 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +22 -18
- package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts +24 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +30 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +13 -5
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +56 -4
- package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop.ts +45 -1
- package/src/resources/extensions/gsd/auto/phases.ts +2 -0
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
- package/src/resources/extensions/gsd/auto-model-selection.ts +9 -1
- package/src/resources/extensions/gsd/auto-start.ts +13 -6
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +68 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +82 -60
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
- package/src/resources/extensions/gsd/commands/handlers/core.ts +58 -11
- package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/forensics.ts +23 -7
- package/src/resources/extensions/gsd/guided-flow.ts +5 -10
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/metrics.ts +12 -1
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
- package/src/resources/extensions/gsd/notification-overlay.ts +24 -7
- package/src/resources/extensions/gsd/notification-store.ts +49 -1
- package/src/resources/extensions/gsd/notification-widget.ts +5 -14
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/src/resources/extensions/gsd/session-model-override.ts +36 -0
- package/src/resources/extensions/gsd/shortcut-defs.ts +49 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +11 -9
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +66 -1
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +36 -51
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/session-model-override.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- /package/dist/web/standalone/.next/static/{4xyaXTn7-shVHaGMcl75o → cYPZv_bAhZk2ms-Pz6vsY}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{4xyaXTn7-shVHaGMcl75o → cYPZv_bAhZk2ms-Pz6vsY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// pi-coding-agent / CredentialCooldownError unit tests
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { CredentialCooldownError } from "./sdk.js";
|
|
7
|
+
|
|
8
|
+
// ─── CredentialCooldownError ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
describe("CredentialCooldownError", () => {
|
|
11
|
+
it("is an instance of Error", () => {
|
|
12
|
+
const err = new CredentialCooldownError("anthropic");
|
|
13
|
+
assert.ok(err instanceof Error);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("has name set to CredentialCooldownError", () => {
|
|
17
|
+
const err = new CredentialCooldownError("anthropic");
|
|
18
|
+
assert.equal(err.name, "CredentialCooldownError");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("has code set to AUTH_COOLDOWN", () => {
|
|
22
|
+
const err = new CredentialCooldownError("anthropic");
|
|
23
|
+
assert.equal(err.code, "AUTH_COOLDOWN");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("message includes the provider name", () => {
|
|
27
|
+
const err = new CredentialCooldownError("openai");
|
|
28
|
+
assert.ok(
|
|
29
|
+
err.message.includes("openai"),
|
|
30
|
+
`Expected message to include provider "openai", got: ${err.message}`,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("message mentions cooldown window", () => {
|
|
35
|
+
const err = new CredentialCooldownError("anthropic");
|
|
36
|
+
assert.ok(
|
|
37
|
+
/cooldown window/i.test(err.message),
|
|
38
|
+
`Expected message to mention "cooldown window", got: ${err.message}`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("retryAfterMs is undefined when not provided", () => {
|
|
43
|
+
const err = new CredentialCooldownError("anthropic");
|
|
44
|
+
assert.equal(err.retryAfterMs, undefined);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("retryAfterMs holds the provided value when specified", () => {
|
|
48
|
+
const err = new CredentialCooldownError("anthropic", 30_000);
|
|
49
|
+
assert.equal(err.retryAfterMs, 30_000);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("retryAfterMs is 0 when explicitly passed as 0", () => {
|
|
53
|
+
const err = new CredentialCooldownError("anthropic", 0);
|
|
54
|
+
assert.equal(err.retryAfterMs, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("code property is readonly and always AUTH_COOLDOWN regardless of provider", () => {
|
|
58
|
+
for (const provider of ["anthropic", "openai", "google", "openrouter"]) {
|
|
59
|
+
const err = new CredentialCooldownError(provider);
|
|
60
|
+
assert.equal(err.code, "AUTH_COOLDOWN", `code should be AUTH_COOLDOWN for provider "${provider}"`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("different providers produce different messages", () => {
|
|
65
|
+
const err1 = new CredentialCooldownError("anthropic");
|
|
66
|
+
const err2 = new CredentialCooldownError("openai");
|
|
67
|
+
assert.notEqual(err1.message, err2.message);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("can be caught as an Error in a try/catch", () => {
|
|
71
|
+
let caught: unknown;
|
|
72
|
+
try {
|
|
73
|
+
throw new CredentialCooldownError("anthropic", 5_000);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
caught = e;
|
|
76
|
+
}
|
|
77
|
+
assert.ok(caught instanceof Error);
|
|
78
|
+
assert.ok(caught instanceof CredentialCooldownError);
|
|
79
|
+
assert.equal((caught as CredentialCooldownError).retryAfterMs, 5_000);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("code property is detectable via plain object check (cross-process pattern)", () => {
|
|
83
|
+
const err = new CredentialCooldownError("anthropic", 15_000);
|
|
84
|
+
// Simulate cross-process serialization: only plain properties survive JSON round-trip
|
|
85
|
+
const plain = { code: err.code, retryAfterMs: err.retryAfterMs, message: err.message };
|
|
86
|
+
assert.equal(plain.code, "AUTH_COOLDOWN");
|
|
87
|
+
assert.equal(plain.retryAfterMs, 15_000);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structured error thrown when all credentials for a provider are in a
|
|
5
|
+
* backoff window. Carries typed metadata so callers (e.g. the auto-loop)
|
|
6
|
+
* can make informed retry decisions instead of string-matching the message.
|
|
7
|
+
*/
|
|
8
|
+
export class CredentialCooldownError extends Error {
|
|
9
|
+
readonly code = "AUTH_COOLDOWN" as const;
|
|
10
|
+
/** Milliseconds until the earliest credential becomes available, or undefined if unknown. */
|
|
11
|
+
readonly retryAfterMs: number | undefined;
|
|
12
|
+
|
|
13
|
+
constructor(provider: string, retryAfterMs?: number) {
|
|
14
|
+
super(
|
|
15
|
+
`All credentials for "${provider}" are in a cooldown window. ` +
|
|
16
|
+
`Please wait a moment and try again, or switch to a different provider.`,
|
|
17
|
+
);
|
|
18
|
+
this.name = "CredentialCooldownError";
|
|
19
|
+
this.retryAfterMs = retryAfterMs;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
2
22
|
import { Agent, type AgentMessage, type ThinkingLevel } from "@gsd/pi-agent-core";
|
|
3
23
|
import type { Message, Model } from "@gsd/pi-ai";
|
|
4
24
|
import { getAgentDir, getDocsPath } from "../config.js";
|
|
@@ -363,8 +383,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
363
383
|
|
|
364
384
|
// Retry key resolution with backoff to handle transient network failures
|
|
365
385
|
// (e.g., OAuth token refresh failing due to brief connectivity loss).
|
|
386
|
+
// When credentials are in a cooldown window (e.g., after a 429), wait
|
|
387
|
+
// for the backoff to expire instead of using fixed delays that are
|
|
388
|
+
// shorter than the cooldown duration.
|
|
366
389
|
const maxAttempts = 3;
|
|
367
390
|
const baseDelayMs = 2000;
|
|
391
|
+
const maxCooldownWaitMs = 60_000; // Don't wait longer than 60s (skip quota-exhausted 30min backoffs)
|
|
368
392
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
369
393
|
const key = await modelRegistry.getApiKeyForProvider(resolvedProvider);
|
|
370
394
|
if (key) return key;
|
|
@@ -379,7 +403,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
379
403
|
const isOAuth = model && modelRegistry.isUsingOAuth(model);
|
|
380
404
|
if (!hasAuth && !isOAuth) break;
|
|
381
405
|
|
|
382
|
-
//
|
|
406
|
+
// If credentials are in a cooldown window, wait for the earliest
|
|
407
|
+
// one to expire rather than using a fixed delay that's too short.
|
|
408
|
+
const backoffExpiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
409
|
+
if (backoffExpiry !== undefined) {
|
|
410
|
+
const waitMs = backoffExpiry - Date.now() + 500; // 500ms buffer
|
|
411
|
+
if (waitMs > 0 && waitMs <= maxCooldownWaitMs) {
|
|
412
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
413
|
+
continue; // Retry immediately after cooldown clears
|
|
414
|
+
}
|
|
415
|
+
if (waitMs > maxCooldownWaitMs) {
|
|
416
|
+
break; // Quota-exhausted or very long backoff — don't block
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Standard exponential backoff for non-cooldown transient failures
|
|
383
421
|
await new Promise(resolve => setTimeout(resolve, baseDelayMs * attempt));
|
|
384
422
|
}
|
|
385
423
|
|
|
@@ -390,10 +428,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
390
428
|
// the retry handler and creating cascading error entries (#3429).
|
|
391
429
|
const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider);
|
|
392
430
|
if (hasAuth) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
431
|
+
const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
432
|
+
const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
|
|
433
|
+
throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
|
|
397
434
|
}
|
|
398
435
|
const model = agent.state.model;
|
|
399
436
|
const isOAuth = model && modelRegistry.isUsingOAuth(model);
|
|
@@ -401,10 +438,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
401
438
|
// If credentials exist but are all in a backoff window (quota / rate-limit),
|
|
402
439
|
// surface a specific message instead of the misleading "Authentication failed".
|
|
403
440
|
if (modelRegistry.authStorage.areAllCredentialsBackedOff(resolvedProvider)) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
441
|
+
const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
442
|
+
const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
|
|
443
|
+
throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
|
|
408
444
|
}
|
|
409
445
|
throw new Error(
|
|
410
446
|
`Authentication failed for "${resolvedProvider}". ` +
|
package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildAuthUrlPresentation } from "../login-dialog.js";
|
|
4
|
+
|
|
5
|
+
describe("LoginDialogComponent", () => {
|
|
6
|
+
test("shows the full OAuth URL when the hyperlink label is truncated", () => {
|
|
7
|
+
const presentation = buildAuthUrlPresentation(
|
|
8
|
+
"https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
|
|
9
|
+
52,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assert.notEqual(
|
|
13
|
+
presentation.displayUrl,
|
|
14
|
+
"https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
|
|
15
|
+
"narrow terminals should still truncate the hyperlink label",
|
|
16
|
+
);
|
|
17
|
+
assert.ok(presentation.fullUrlLines.length > 1, "truncated URLs should expose wrapped full-url lines");
|
|
18
|
+
assert.match(presentation.fullUrlLines[0] ?? "", /https:\/\/auth\.example\.com\/device\?code=ABCD-1234&/);
|
|
19
|
+
assert.match(
|
|
20
|
+
presentation.fullUrlLines[presentation.fullUrlLines.length - 1] ?? "",
|
|
21
|
+
/state=needs-full-visibility/,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -7,6 +7,27 @@ import { theme } from "../theme/theme.js";
|
|
|
7
7
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
8
|
import { keyHint } from "./keybinding-hints.js";
|
|
9
9
|
|
|
10
|
+
function wrapPlainText(text: string, width: number): string[] {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
const safeWidth = Math.max(1, width);
|
|
13
|
+
for (let idx = 0; idx < text.length; idx += safeWidth) {
|
|
14
|
+
lines.push(text.slice(idx, idx + safeWidth));
|
|
15
|
+
}
|
|
16
|
+
return lines.length > 0 ? lines : [""];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildAuthUrlPresentation(url: string, terminalColumns: number): {
|
|
20
|
+
displayUrl: string;
|
|
21
|
+
fullUrlLines: string[];
|
|
22
|
+
} {
|
|
23
|
+
const maxUrlWidth = Math.max(20, terminalColumns - 4);
|
|
24
|
+
const displayUrl = truncateToWidth(url, maxUrlWidth);
|
|
25
|
+
return {
|
|
26
|
+
displayUrl,
|
|
27
|
+
fullUrlLines: displayUrl === url ? [] : wrapPlainText(url, maxUrlWidth),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
/**
|
|
11
32
|
* Login dialog component - replaces editor during OAuth login flow.
|
|
12
33
|
*
|
|
@@ -124,14 +145,21 @@ export class LoginDialogComponent extends Container implements Focusable {
|
|
|
124
145
|
|
|
125
146
|
// Truncate the visible URL text so it never wraps (which would break
|
|
126
147
|
// the OSC 8 hyperlink). The full URL is still the link target.
|
|
127
|
-
const
|
|
128
|
-
const displayUrl = truncateToWidth(url, maxUrlWidth);
|
|
148
|
+
const { displayUrl, fullUrlLines } = buildAuthUrlPresentation(url, this.tui.terminal.columns);
|
|
129
149
|
const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`;
|
|
130
150
|
this.contentContainer.addChild(new Text(urlLink, 1, 0));
|
|
131
151
|
|
|
132
152
|
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
|
133
153
|
this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0));
|
|
134
154
|
|
|
155
|
+
if (fullUrlLines.length > 0) {
|
|
156
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
157
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", "Full URL:"), 1, 0));
|
|
158
|
+
for (const line of fullUrlLines) {
|
|
159
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
if (instructions) {
|
|
136
164
|
this.contentContainer.addChild(new Spacer(1));
|
|
137
165
|
this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
|
|
@@ -325,6 +325,29 @@ export class ToolExecutionComponent extends Container {
|
|
|
325
325
|
this.maybeConvertImagesForKitty();
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Finalize a pending tool call as failed/interrupted while preserving any streamed partial output.
|
|
330
|
+
*/
|
|
331
|
+
completeWithError(message?: string): void {
|
|
332
|
+
this.isPartial = false;
|
|
333
|
+
if (this.result) {
|
|
334
|
+
let content = this.result.content;
|
|
335
|
+
if (message) {
|
|
336
|
+
const alreadyHasMessage = content.some((block) => block.type === "text" && block.text === message);
|
|
337
|
+
if (!alreadyHasMessage) {
|
|
338
|
+
content = [...content, { type: "text", text: message }];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.result = { ...this.result, content, isError: true };
|
|
342
|
+
} else {
|
|
343
|
+
this.result = {
|
|
344
|
+
content: message ? [{ type: "text", text: message }] : [],
|
|
345
|
+
isError: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
this.updateDisplay();
|
|
349
|
+
}
|
|
350
|
+
|
|
328
351
|
/**
|
|
329
352
|
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
|
330
353
|
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
|
@@ -652,6 +675,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
652
675
|
text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
|
|
653
676
|
|
|
654
677
|
if (this.result) {
|
|
678
|
+
if (this.result.isError) {
|
|
679
|
+
const errorText = this.getTextOutput().trim() || "read failed";
|
|
680
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
681
|
+
return text;
|
|
682
|
+
}
|
|
683
|
+
|
|
655
684
|
const rawOutput = this.getTextOutput();
|
|
656
685
|
// Strip hashline prefixes (e.g. "1#BQ:content") for TUI display
|
|
657
686
|
const output = rawOutput.replace(/^(\s*)\d+#[ZPMQVRWSNKTXJBYH]{2}:/gm, "$1");
|
|
@@ -804,6 +833,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
804
833
|
}
|
|
805
834
|
|
|
806
835
|
if (this.result) {
|
|
836
|
+
if (this.result.isError) {
|
|
837
|
+
const errorText = this.getTextOutput().trim() || "ls failed";
|
|
838
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
839
|
+
return text;
|
|
840
|
+
}
|
|
841
|
+
|
|
807
842
|
const output = this.getTextOutput().trim();
|
|
808
843
|
if (output) {
|
|
809
844
|
const lines = output.split("\n");
|
|
@@ -846,6 +881,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
846
881
|
}
|
|
847
882
|
|
|
848
883
|
if (this.result) {
|
|
884
|
+
if (this.result.isError) {
|
|
885
|
+
const errorText = this.getTextOutput().trim() || "find failed";
|
|
886
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
887
|
+
return text;
|
|
888
|
+
}
|
|
889
|
+
|
|
849
890
|
const output = this.getTextOutput().trim();
|
|
850
891
|
if (output) {
|
|
851
892
|
const lines = output.split("\n");
|
|
@@ -892,6 +933,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
892
933
|
}
|
|
893
934
|
|
|
894
935
|
if (this.result) {
|
|
936
|
+
if (this.result.isError) {
|
|
937
|
+
const errorText = this.getTextOutput().trim() || "grep failed";
|
|
938
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
939
|
+
return text;
|
|
940
|
+
}
|
|
941
|
+
|
|
895
942
|
const output = this.getTextOutput().trim();
|
|
896
943
|
if (output) {
|
|
897
944
|
const lines = output.split("\n");
|
|
@@ -369,8 +369,13 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
369
369
|
if (!errorMessage) {
|
|
370
370
|
errorMessage = host.streamingMessage.errorMessage || "Error";
|
|
371
371
|
}
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
const pendingComponents = Array.from(host.pendingTools.values());
|
|
373
|
+
if (pendingComponents.length > 0) {
|
|
374
|
+
const [first, ...rest] = pendingComponents;
|
|
375
|
+
first.completeWithError(errorMessage);
|
|
376
|
+
for (const component of rest) {
|
|
377
|
+
component.completeWithError();
|
|
378
|
+
}
|
|
374
379
|
}
|
|
375
380
|
host.pendingTools.clear();
|
|
376
381
|
} else {
|
|
@@ -1785,7 +1785,7 @@ export class InteractiveMode {
|
|
|
1785
1785
|
} else if (type === "warning") {
|
|
1786
1786
|
this.showWarning(message);
|
|
1787
1787
|
} else {
|
|
1788
|
-
this.showStatus(message);
|
|
1788
|
+
this.showStatus(message, { append: true });
|
|
1789
1789
|
}
|
|
1790
1790
|
}
|
|
1791
1791
|
|
|
@@ -2052,12 +2052,13 @@ export class InteractiveMode {
|
|
|
2052
2052
|
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
2053
2053
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2054
2054
|
*/
|
|
2055
|
-
private showStatus(message: string): void {
|
|
2055
|
+
private showStatus(message: string, options?: { append?: boolean }): void {
|
|
2056
|
+
const append = options?.append ?? false;
|
|
2056
2057
|
const children = this.chatContainer.children;
|
|
2057
2058
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
2058
2059
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
2059
2060
|
|
|
2060
|
-
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
2061
|
+
if (!append && last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
2061
2062
|
this.lastStatusText.setText(theme.fg("dim", message));
|
|
2062
2063
|
this.ui.requestRender();
|
|
2063
2064
|
return;
|
|
@@ -499,12 +499,14 @@ function handleHotkeysCommand(ctx: SlashCommandContext): void {
|
|
|
499
499
|
const suspend = getAppKeyDisplay(ctx.keybindings, "suspend");
|
|
500
500
|
const cycleThinkingLevel = getAppKeyDisplay(ctx.keybindings, "cycleThinkingLevel");
|
|
501
501
|
const cycleModelForward = getAppKeyDisplay(ctx.keybindings, "cycleModelForward");
|
|
502
|
+
const cycleModelBackward = getAppKeyDisplay(ctx.keybindings, "cycleModelBackward");
|
|
502
503
|
const selectModel = getAppKeyDisplay(ctx.keybindings, "selectModel");
|
|
503
504
|
const expandTools = getAppKeyDisplay(ctx.keybindings, "expandTools");
|
|
504
505
|
const toggleThinking = getAppKeyDisplay(ctx.keybindings, "toggleThinking");
|
|
505
506
|
const externalEditor = getAppKeyDisplay(ctx.keybindings, "externalEditor");
|
|
506
507
|
const followUp = getAppKeyDisplay(ctx.keybindings, "followUp");
|
|
507
508
|
const dequeue = getAppKeyDisplay(ctx.keybindings, "dequeue");
|
|
509
|
+
const pasteImage = getAppKeyDisplay(ctx.keybindings, "pasteImage");
|
|
508
510
|
|
|
509
511
|
let hotkeys = `
|
|
510
512
|
**Navigation**
|
|
@@ -540,14 +542,14 @@ function handleHotkeysCommand(ctx: SlashCommandContext): void {
|
|
|
540
542
|
| \`${exit}\` | Exit (when editor is empty) |
|
|
541
543
|
| \`${suspend}\` | Suspend to background |
|
|
542
544
|
| \`${cycleThinkingLevel}\` | Cycle thinking level |
|
|
543
|
-
| \`${cycleModelForward}\` | Cycle models |
|
|
545
|
+
| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
|
|
544
546
|
| \`${selectModel}\` | Open model selector |
|
|
545
547
|
| \`${expandTools}\` | Toggle tool output expansion |
|
|
546
548
|
| \`${toggleThinking}\` | Toggle thinking block visibility |
|
|
547
549
|
| \`${externalEditor}\` | Edit message in external editor |
|
|
548
550
|
| \`${followUp}\` | Queue follow-up message |
|
|
549
551
|
| \`${dequeue}\` | Restore queued messages |
|
|
550
|
-
| \`
|
|
552
|
+
| \`${pasteImage}\` | Paste image from clipboard |
|
|
551
553
|
| \`/\` | Slash commands |
|
|
552
554
|
| \`!\` | Run bash command |
|
|
553
555
|
| \`!!\` | Run bash command (excluded from context) |
|
|
@@ -275,7 +275,7 @@ Work flows through these phases. Each phase produces a file.
|
|
|
275
275
|
**How to do it manually:**
|
|
276
276
|
1. Read the roadmap to understand the scope.
|
|
277
277
|
2. Identify 3-5 gray areas — implementation decisions the user cares about.
|
|
278
|
-
3. Use `ask_user_questions` to discuss each area.
|
|
278
|
+
3. Use `ask_user_questions` to discuss each area, one round at a time. Never fabricate user input; wait for the user's actual response before the next round.
|
|
279
279
|
4. Write decisions to the appropriate context file (`M###-CONTEXT.md` or `S##-CONTEXT.md`).
|
|
280
280
|
5. Do NOT discuss how to implement — only what the user wants.
|
|
281
281
|
|
|
@@ -118,6 +118,18 @@ function createAssistantStream(): AssistantMessageEventStream {
|
|
|
118
118
|
) as AssistantMessageEventStream;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export function getResultErrorMessage(result: SDKResultMessage): string {
|
|
122
|
+
if ("errors" in result && Array.isArray(result.errors) && result.errors.length > 0) {
|
|
123
|
+
return result.errors.join("; ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if ("result" in result && typeof result.result === "string" && result.result.trim().length > 0) {
|
|
127
|
+
return result.result.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result.subtype === "success" ? "claude_code_request_failed" : result.subtype;
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
// ---------------------------------------------------------------------------
|
|
122
134
|
// Claude binary resolution
|
|
123
135
|
// ---------------------------------------------------------------------------
|
|
@@ -882,11 +894,7 @@ async function pumpSdkMessages(
|
|
|
882
894
|
};
|
|
883
895
|
|
|
884
896
|
if (result.is_error) {
|
|
885
|
-
|
|
886
|
-
"errors" in result
|
|
887
|
-
? (result as any).errors?.join("; ")
|
|
888
|
-
: result.subtype;
|
|
889
|
-
finalMessage.errorMessage = errText;
|
|
897
|
+
finalMessage.errorMessage = getResultErrorMessage(result);
|
|
890
898
|
stream.push({ type: "error", reason: "error", error: finalMessage });
|
|
891
899
|
} else {
|
|
892
900
|
stream.push({ type: "done", reason: "stop", message: finalMessage });
|
|
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import {
|
|
7
7
|
makeStreamExhaustedErrorMessage,
|
|
8
|
+
getResultErrorMessage,
|
|
8
9
|
buildPromptFromContext,
|
|
9
10
|
buildSdkOptions,
|
|
10
11
|
createClaudeCodeElicitationHandler,
|
|
@@ -40,6 +41,57 @@ describe("stream-adapter — exhausted stream fallback (#2575)", () => {
|
|
|
40
41
|
});
|
|
41
42
|
});
|
|
42
43
|
|
|
44
|
+
describe("stream-adapter — result error text (#3776)", () => {
|
|
45
|
+
test("prefers SDK result text when an error arrives with subtype success", () => {
|
|
46
|
+
const message = getResultErrorMessage({
|
|
47
|
+
type: "result",
|
|
48
|
+
subtype: "success",
|
|
49
|
+
uuid: "uuid-1",
|
|
50
|
+
session_id: "session-1",
|
|
51
|
+
duration_ms: 1,
|
|
52
|
+
duration_api_ms: 1,
|
|
53
|
+
is_error: true,
|
|
54
|
+
num_turns: 1,
|
|
55
|
+
result: 'API Error: 529 {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
|
|
56
|
+
stop_reason: null,
|
|
57
|
+
total_cost_usd: 0,
|
|
58
|
+
usage: {
|
|
59
|
+
input_tokens: 0,
|
|
60
|
+
output_tokens: 0,
|
|
61
|
+
cache_read_input_tokens: 0,
|
|
62
|
+
cache_creation_input_tokens: 0,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.match(message, /API Error: 529/);
|
|
67
|
+
assert.doesNotMatch(message, /^success$/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("falls back to a stable classifier when success errors have no text", () => {
|
|
71
|
+
const message = getResultErrorMessage({
|
|
72
|
+
type: "result",
|
|
73
|
+
subtype: "success",
|
|
74
|
+
uuid: "uuid-2",
|
|
75
|
+
session_id: "session-2",
|
|
76
|
+
duration_ms: 1,
|
|
77
|
+
duration_api_ms: 1,
|
|
78
|
+
is_error: true,
|
|
79
|
+
num_turns: 1,
|
|
80
|
+
result: " ",
|
|
81
|
+
stop_reason: null,
|
|
82
|
+
total_cost_usd: 0,
|
|
83
|
+
usage: {
|
|
84
|
+
input_tokens: 0,
|
|
85
|
+
output_tokens: 0,
|
|
86
|
+
cache_read_input_tokens: 0,
|
|
87
|
+
cache_creation_input_tokens: 0,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
assert.equal(message, "claude_code_request_failed");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
43
95
|
// ---------------------------------------------------------------------------
|
|
44
96
|
// Bug #2859 — stateless provider regression tests
|
|
45
97
|
// ---------------------------------------------------------------------------
|
|
@@ -598,9 +650,9 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
598
650
|
requestedSchema: {
|
|
599
651
|
type: "object" as const,
|
|
600
652
|
properties: {
|
|
601
|
-
|
|
653
|
+
TEST_SECURE_FIELD: {
|
|
602
654
|
type: "string",
|
|
603
|
-
title: "
|
|
655
|
+
title: "TEST_SECURE_FIELD",
|
|
604
656
|
description: "Format: Your secure testing password\nLeave empty to skip.",
|
|
605
657
|
},
|
|
606
658
|
},
|
|
@@ -611,7 +663,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
611
663
|
const handler = createClaudeCodeElicitationHandler({
|
|
612
664
|
input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
|
|
613
665
|
inputCalls.push({ opts });
|
|
614
|
-
return "
|
|
666
|
+
return "example-secure-input";
|
|
615
667
|
},
|
|
616
668
|
} as any);
|
|
617
669
|
assert.ok(handler);
|
|
@@ -620,7 +672,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
620
672
|
assert.deepEqual(result, {
|
|
621
673
|
action: "accept",
|
|
622
674
|
content: {
|
|
623
|
-
|
|
675
|
+
TEST_SECURE_FIELD: "example-secure-input",
|
|
624
676
|
},
|
|
625
677
|
});
|
|
626
678
|
assert.equal(inputCalls.length, 1);
|
|
@@ -46,3 +46,41 @@ export function isInfrastructureError(err: unknown): string | null {
|
|
|
46
46
|
if (msg.includes("database disk image is malformed")) return "SQLITE_CORRUPT";
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default wait duration when a cooldown error is detected but no specific
|
|
52
|
+
* expiry is available from AuthStorage (e.g., error propagated across
|
|
53
|
+
* process boundary without structured backoff data).
|
|
54
|
+
*/
|
|
55
|
+
export const COOLDOWN_FALLBACK_WAIT_MS = 35_000; // 35s — slightly longer than the 30s rate-limit backoff
|
|
56
|
+
|
|
57
|
+
/** Maximum consecutive cooldown retries before the auto-loop gives up. */
|
|
58
|
+
export const MAX_COOLDOWN_RETRIES = 5;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect whether an error is a transient credential cooldown that should
|
|
62
|
+
* be waited out rather than counted as a consecutive failure.
|
|
63
|
+
*
|
|
64
|
+
* Prefers the structured `CredentialCooldownError` (code: AUTH_COOLDOWN)
|
|
65
|
+
* thrown by sdk.ts. Falls back to message matching for errors that
|
|
66
|
+
* propagated across process boundaries without the typed class.
|
|
67
|
+
*/
|
|
68
|
+
export function isTransientCooldownError(err: unknown): boolean {
|
|
69
|
+
if (err && typeof err === "object" && (err as Record<string, unknown>).code === "AUTH_COOLDOWN") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// Fallback: message match for cross-process error propagation
|
|
73
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
return /in a cooldown window/i.test(msg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract retryAfterMs from a CredentialCooldownError, if available.
|
|
79
|
+
* Returns undefined for unstructured errors or when no retry hint exists.
|
|
80
|
+
*/
|
|
81
|
+
export function getCooldownRetryAfterMs(err: unknown): number | undefined {
|
|
82
|
+
if (err && typeof err === "object" && (err as Record<string, unknown>).code === "AUTH_COOLDOWN") {
|
|
83
|
+
return (err as Record<string, unknown>).retryAfterMs as number | undefined;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
@@ -211,6 +211,8 @@ export interface LoopDeps {
|
|
|
211
211
|
verbose: boolean,
|
|
212
212
|
startModel: { provider: string; id: string } | null,
|
|
213
213
|
retryContext?: { isRetry: boolean; previousTier?: string },
|
|
214
|
+
isAutoMode?: boolean,
|
|
215
|
+
sessionModelOverride?: { provider: string; id: string } | null,
|
|
214
216
|
) => Promise<{
|
|
215
217
|
routing: { tier: string; modelDowngraded: boolean } | null;
|
|
216
218
|
appliedModel: { provider: string; id: string } | null;
|