@kodrunhq/opencode-autopilot 0.1.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/LICENSE +21 -0
- package/README.md +1 -0
- package/assets/agents/placeholder-agent.md +13 -0
- package/assets/commands/configure.md +17 -0
- package/assets/commands/new-agent.md +16 -0
- package/assets/commands/new-command.md +15 -0
- package/assets/commands/new-skill.md +15 -0
- package/assets/commands/review-pr.md +49 -0
- package/assets/skills/.gitkeep +0 -0
- package/assets/skills/coding-standards/SKILL.md +327 -0
- package/package.json +52 -0
- package/src/agents/autopilot.ts +42 -0
- package/src/agents/documenter.ts +44 -0
- package/src/agents/index.ts +49 -0
- package/src/agents/metaprompter.ts +50 -0
- package/src/agents/pipeline/index.ts +25 -0
- package/src/agents/pipeline/oc-architect.ts +49 -0
- package/src/agents/pipeline/oc-challenger.ts +44 -0
- package/src/agents/pipeline/oc-critic.ts +42 -0
- package/src/agents/pipeline/oc-explorer.ts +46 -0
- package/src/agents/pipeline/oc-implementer.ts +56 -0
- package/src/agents/pipeline/oc-planner.ts +45 -0
- package/src/agents/pipeline/oc-researcher.ts +46 -0
- package/src/agents/pipeline/oc-retrospector.ts +42 -0
- package/src/agents/pipeline/oc-reviewer.ts +44 -0
- package/src/agents/pipeline/oc-shipper.ts +42 -0
- package/src/agents/pr-reviewer.ts +74 -0
- package/src/agents/researcher.ts +43 -0
- package/src/config.ts +168 -0
- package/src/index.ts +152 -0
- package/src/installer.ts +130 -0
- package/src/orchestrator/arena.ts +41 -0
- package/src/orchestrator/artifacts.ts +28 -0
- package/src/orchestrator/confidence.ts +59 -0
- package/src/orchestrator/fallback/chat-message-handler.ts +49 -0
- package/src/orchestrator/fallback/error-classifier.ts +148 -0
- package/src/orchestrator/fallback/event-handler.ts +235 -0
- package/src/orchestrator/fallback/fallback-config.ts +16 -0
- package/src/orchestrator/fallback/fallback-manager.ts +323 -0
- package/src/orchestrator/fallback/fallback-state.ts +120 -0
- package/src/orchestrator/fallback/index.ts +11 -0
- package/src/orchestrator/fallback/message-replay.ts +40 -0
- package/src/orchestrator/fallback/resolve-chain.ts +34 -0
- package/src/orchestrator/fallback/tool-execute-handler.ts +44 -0
- package/src/orchestrator/fallback/types.ts +46 -0
- package/src/orchestrator/handlers/architect.ts +114 -0
- package/src/orchestrator/handlers/build.ts +363 -0
- package/src/orchestrator/handlers/challenge.ts +41 -0
- package/src/orchestrator/handlers/explore.ts +9 -0
- package/src/orchestrator/handlers/index.ts +21 -0
- package/src/orchestrator/handlers/plan.ts +35 -0
- package/src/orchestrator/handlers/recon.ts +40 -0
- package/src/orchestrator/handlers/retrospective.ts +123 -0
- package/src/orchestrator/handlers/ship.ts +38 -0
- package/src/orchestrator/handlers/types.ts +31 -0
- package/src/orchestrator/lesson-injection.ts +80 -0
- package/src/orchestrator/lesson-memory.ts +110 -0
- package/src/orchestrator/lesson-schemas.ts +24 -0
- package/src/orchestrator/lesson-types.ts +6 -0
- package/src/orchestrator/phase.ts +76 -0
- package/src/orchestrator/plan.ts +43 -0
- package/src/orchestrator/schemas.ts +86 -0
- package/src/orchestrator/skill-injection.ts +52 -0
- package/src/orchestrator/state.ts +80 -0
- package/src/orchestrator/types.ts +20 -0
- package/src/review/agent-catalog.ts +439 -0
- package/src/review/agents/auth-flow-verifier.ts +47 -0
- package/src/review/agents/code-quality-auditor.ts +51 -0
- package/src/review/agents/concurrency-checker.ts +47 -0
- package/src/review/agents/contract-verifier.ts +45 -0
- package/src/review/agents/database-auditor.ts +47 -0
- package/src/review/agents/dead-code-scanner.ts +47 -0
- package/src/review/agents/go-idioms-auditor.ts +46 -0
- package/src/review/agents/index.ts +82 -0
- package/src/review/agents/logic-auditor.ts +47 -0
- package/src/review/agents/product-thinker.ts +49 -0
- package/src/review/agents/python-django-auditor.ts +46 -0
- package/src/review/agents/react-patterns-auditor.ts +46 -0
- package/src/review/agents/red-team.ts +49 -0
- package/src/review/agents/rust-safety-auditor.ts +46 -0
- package/src/review/agents/scope-intent-verifier.ts +45 -0
- package/src/review/agents/security-auditor.ts +47 -0
- package/src/review/agents/silent-failure-hunter.ts +45 -0
- package/src/review/agents/spec-checker.ts +45 -0
- package/src/review/agents/state-mgmt-auditor.ts +46 -0
- package/src/review/agents/test-interrogator.ts +43 -0
- package/src/review/agents/type-soundness.ts +46 -0
- package/src/review/agents/wiring-inspector.ts +46 -0
- package/src/review/cross-verification.ts +71 -0
- package/src/review/finding-builder.ts +74 -0
- package/src/review/fix-cycle.ts +146 -0
- package/src/review/memory.ts +114 -0
- package/src/review/pipeline.ts +258 -0
- package/src/review/report.ts +141 -0
- package/src/review/sanitize.ts +8 -0
- package/src/review/schemas.ts +75 -0
- package/src/review/selection.ts +98 -0
- package/src/review/severity.ts +71 -0
- package/src/review/stack-gate.ts +127 -0
- package/src/review/types.ts +43 -0
- package/src/templates/agent-template.ts +47 -0
- package/src/templates/command-template.ts +29 -0
- package/src/templates/skill-template.ts +42 -0
- package/src/tools/confidence.ts +93 -0
- package/src/tools/create-agent.ts +81 -0
- package/src/tools/create-command.ts +74 -0
- package/src/tools/create-skill.ts +74 -0
- package/src/tools/forensics.ts +88 -0
- package/src/tools/orchestrate.ts +310 -0
- package/src/tools/phase.ts +92 -0
- package/src/tools/placeholder.ts +11 -0
- package/src/tools/plan.ts +56 -0
- package/src/tools/review.ts +295 -0
- package/src/tools/state.ts +112 -0
- package/src/utils/fs-helpers.ts +39 -0
- package/src/utils/gitignore.ts +27 -0
- package/src/utils/paths.ts +17 -0
- package/src/utils/validators.ts +57 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ErrorType } from "./types";
|
|
2
|
+
|
|
3
|
+
export const RETRYABLE_ERROR_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
4
|
+
/rate.?limit/i,
|
|
5
|
+
/too.?many.?requests/i,
|
|
6
|
+
/quota.?exceeded/i,
|
|
7
|
+
/quota.?protection/i,
|
|
8
|
+
/key.?limit.?exceeded/i,
|
|
9
|
+
/usage\s+limit\s+has\s+been\s+reached/i,
|
|
10
|
+
/service.?unavailable/i,
|
|
11
|
+
/overloaded/i,
|
|
12
|
+
/temporarily.?unavailable/i,
|
|
13
|
+
/try.?again/i,
|
|
14
|
+
/credit.*balance.*too.*low/i,
|
|
15
|
+
/insufficient.?(?:credits?|funds?|balance)/i,
|
|
16
|
+
/(?:^|\s)429(?:\s|$)/,
|
|
17
|
+
/(?:^|\s)503(?:\s|$)/,
|
|
18
|
+
/(?:^|\s)529(?:\s|$)/,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracts a human-readable error message from an unknown error value.
|
|
23
|
+
* Handles nested error.error.message, error.message, error.error, and string errors.
|
|
24
|
+
*/
|
|
25
|
+
export function getErrorMessage(error: unknown): string {
|
|
26
|
+
if (error === null || error === undefined) return "";
|
|
27
|
+
if (typeof error === "string") return error;
|
|
28
|
+
|
|
29
|
+
if (typeof error === "object") {
|
|
30
|
+
const obj = error as Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
// Check nested error.error.message first
|
|
33
|
+
if (obj.error && typeof obj.error === "object") {
|
|
34
|
+
const nested = obj.error as Record<string, unknown>;
|
|
35
|
+
if (typeof nested.message === "string") return nested.message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check error.message
|
|
39
|
+
if (typeof obj.message === "string") return obj.message;
|
|
40
|
+
|
|
41
|
+
// Check error.error as string
|
|
42
|
+
if (typeof obj.error === "string") return obj.error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extracts a status code from an error if it matches the retryable set.
|
|
50
|
+
* Checks error.status, error.statusCode, and message text.
|
|
51
|
+
*/
|
|
52
|
+
export function extractStatusCode(error: unknown, retryOnErrors: readonly number[]): number | null {
|
|
53
|
+
if (error !== null && typeof error === "object") {
|
|
54
|
+
const obj = error as Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
if (typeof obj.status === "number" && retryOnErrors.includes(obj.status)) {
|
|
57
|
+
return obj.status;
|
|
58
|
+
}
|
|
59
|
+
if (typeof obj.statusCode === "number" && retryOnErrors.includes(obj.statusCode)) {
|
|
60
|
+
return obj.statusCode;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract from message text — restrict to 4xx/5xx to avoid false positives
|
|
65
|
+
const message = getErrorMessage(error);
|
|
66
|
+
const matches = message.matchAll(/\b([45]\d{2})\b/g);
|
|
67
|
+
for (const m of matches) {
|
|
68
|
+
const code = Number.parseInt(m[1], 10);
|
|
69
|
+
if (retryOnErrors.includes(code)) return code;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classifies an error into a known ErrorType category based on message patterns and status codes.
|
|
77
|
+
*/
|
|
78
|
+
export function classifyErrorType(error: unknown): ErrorType {
|
|
79
|
+
const message = getErrorMessage(error);
|
|
80
|
+
const lowerMessage = message.toLowerCase();
|
|
81
|
+
|
|
82
|
+
// Check status code first
|
|
83
|
+
if (error !== null && typeof error === "object") {
|
|
84
|
+
const obj = error as Record<string, unknown>;
|
|
85
|
+
if (obj.status === 429 || obj.statusCode === 429) return "rate_limit";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check message patterns
|
|
89
|
+
if (
|
|
90
|
+
/api.?key/i.test(message) &&
|
|
91
|
+
/missing|no\s|not\s+(?:provided|found|set|configured)/i.test(message)
|
|
92
|
+
)
|
|
93
|
+
return "missing_api_key";
|
|
94
|
+
if (/model.*not.*(?:found|exist)/i.test(message)) return "model_not_found";
|
|
95
|
+
if (/content.?filter/i.test(message)) return "content_filter";
|
|
96
|
+
if (/context.?length/i.test(message)) return "context_length";
|
|
97
|
+
if (/rate.?limit/i.test(lowerMessage) || /too.?many.?requests/i.test(lowerMessage))
|
|
98
|
+
return "rate_limit";
|
|
99
|
+
if (
|
|
100
|
+
/quota.?exceeded/i.test(lowerMessage) ||
|
|
101
|
+
/insufficient.?(?:credits?|funds?|balance)/i.test(lowerMessage)
|
|
102
|
+
)
|
|
103
|
+
return "quota_exceeded";
|
|
104
|
+
if (/service.?unavailable/i.test(lowerMessage) || /overloaded/i.test(lowerMessage))
|
|
105
|
+
return "service_unavailable";
|
|
106
|
+
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Determines whether an error is retryable by another model.
|
|
112
|
+
* Checks status codes, built-in patterns, error type, and user-provided patterns.
|
|
113
|
+
*/
|
|
114
|
+
export function isRetryableError(
|
|
115
|
+
error: unknown,
|
|
116
|
+
retryOnErrors: readonly number[],
|
|
117
|
+
userPatterns?: readonly string[],
|
|
118
|
+
): boolean {
|
|
119
|
+
const errorType = classifyErrorType(error);
|
|
120
|
+
|
|
121
|
+
// content_filter: same content will fail on any model.
|
|
122
|
+
// context_length: replay would fail without truncation; caller must truncate first.
|
|
123
|
+
if (errorType === "content_filter" || errorType === "context_length") return false;
|
|
124
|
+
|
|
125
|
+
if (errorType === "missing_api_key" || errorType === "model_not_found") return true;
|
|
126
|
+
|
|
127
|
+
const statusCode = extractStatusCode(error, retryOnErrors);
|
|
128
|
+
if (statusCode !== null && retryOnErrors.includes(statusCode)) return true;
|
|
129
|
+
|
|
130
|
+
const message = getErrorMessage(error);
|
|
131
|
+
if (RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(message))) return true;
|
|
132
|
+
|
|
133
|
+
if (userPatterns) {
|
|
134
|
+
for (const patternStr of userPatterns) {
|
|
135
|
+
// ReDoS protection: reject patterns with nested quantifiers or backtracking risk
|
|
136
|
+
if (/(\+|\*|\{)\s*\)(\+|\*|\{|\?)/.test(patternStr)) continue;
|
|
137
|
+
if (/\(.*\|.*\)(\+|\*|\{)/.test(patternStr)) continue;
|
|
138
|
+
try {
|
|
139
|
+
const re = new RegExp(patternStr, "i");
|
|
140
|
+
if (re.test(message)) return true;
|
|
141
|
+
} catch {
|
|
142
|
+
/* Invalid regex -- skip */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { FallbackConfig } from "./fallback-config";
|
|
2
|
+
import type { FallbackManager } from "./fallback-manager";
|
|
3
|
+
import { replayWithDegradation } from "./message-replay";
|
|
4
|
+
import type { MessagePart } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SDK operations interface for dependency injection.
|
|
8
|
+
* Enables testing without the OpenCode runtime.
|
|
9
|
+
*/
|
|
10
|
+
export interface SdkOperations {
|
|
11
|
+
readonly abortSession: (sessionID: string) => Promise<void>;
|
|
12
|
+
readonly getSessionMessages: (sessionID: string) => Promise<readonly MessagePart[]>;
|
|
13
|
+
readonly promptAsync: (
|
|
14
|
+
sessionID: string,
|
|
15
|
+
model: { readonly providerID: string; readonly modelID: string },
|
|
16
|
+
parts: readonly MessagePart[],
|
|
17
|
+
) => Promise<void>;
|
|
18
|
+
readonly showToast: (
|
|
19
|
+
title: string,
|
|
20
|
+
message: string,
|
|
21
|
+
variant: "info" | "warning" | "error",
|
|
22
|
+
) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EventHandlerDeps {
|
|
26
|
+
readonly manager: FallbackManager;
|
|
27
|
+
readonly sdk: SdkOperations;
|
|
28
|
+
readonly config: FallbackConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses a "provider/model" string into { providerID, modelID }.
|
|
33
|
+
* Splits on the first "/" only. Returns null if no "/" found.
|
|
34
|
+
*/
|
|
35
|
+
export function parseModelString(
|
|
36
|
+
model: string,
|
|
37
|
+
): { readonly providerID: string; readonly modelID: string } | null {
|
|
38
|
+
const slashIndex = model.indexOf("/");
|
|
39
|
+
if (slashIndex <= 0) return null;
|
|
40
|
+
return {
|
|
41
|
+
providerID: model.slice(0, slashIndex),
|
|
42
|
+
modelID: model.slice(slashIndex + 1),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extracts a session ID from event properties.
|
|
48
|
+
* Supports both `properties.sessionID` and `properties.info.sessionID`.
|
|
49
|
+
*/
|
|
50
|
+
function extractSessionID(properties: Record<string, unknown>): string | undefined {
|
|
51
|
+
if (typeof properties.sessionID === "string") return properties.sessionID;
|
|
52
|
+
if (
|
|
53
|
+
properties.info !== null &&
|
|
54
|
+
typeof properties.info === "object" &&
|
|
55
|
+
typeof (properties.info as Record<string, unknown>).sessionID === "string"
|
|
56
|
+
) {
|
|
57
|
+
return (properties.info as Record<string, unknown>).sessionID as string;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Factory that creates an event handler function bound to fallback dependencies.
|
|
64
|
+
* The returned handler routes OpenCode events to the FallbackManager.
|
|
65
|
+
*/
|
|
66
|
+
export function createEventHandler(deps: EventHandlerDeps) {
|
|
67
|
+
const { manager, sdk, config } = deps;
|
|
68
|
+
|
|
69
|
+
return async (input: {
|
|
70
|
+
readonly event: { readonly type: string; readonly [key: string]: unknown };
|
|
71
|
+
}): Promise<void> => {
|
|
72
|
+
const { event } = input;
|
|
73
|
+
const properties = (event.properties ?? {}) as Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case "session.created": {
|
|
77
|
+
const info = properties.info as
|
|
78
|
+
| { id?: string; model?: string; parentID?: string | null; agent?: string }
|
|
79
|
+
| undefined;
|
|
80
|
+
if (!info?.id) return;
|
|
81
|
+
|
|
82
|
+
const model = typeof info.model === "string" ? info.model : "";
|
|
83
|
+
const parentID = info.parentID !== undefined ? info.parentID : undefined;
|
|
84
|
+
const agentName = typeof info.agent === "string" ? info.agent : undefined;
|
|
85
|
+
manager.initSession(info.id, model, parentID, agentName);
|
|
86
|
+
|
|
87
|
+
// Start TTFT timeout only when fallback is enabled and timeout configured.
|
|
88
|
+
// Without a fallback chain, a TTFT abort would just fail the session.
|
|
89
|
+
if (model && config.enabled && config.timeoutSeconds > 0) {
|
|
90
|
+
manager.startTtftTimeout(info.id, () => {
|
|
91
|
+
// Guard: skip if session was cleaned up before timer fires
|
|
92
|
+
if (!manager.getSessionState(info.id as string)) return;
|
|
93
|
+
// On TTFT timeout, abort session to trigger fallback via session.error
|
|
94
|
+
sdk.abortSession(info.id as string).catch(() => {
|
|
95
|
+
// Best-effort abort; session.error will handle the result
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "session.deleted": {
|
|
103
|
+
const info = properties.info as { id?: string } | undefined;
|
|
104
|
+
if (info?.id) {
|
|
105
|
+
manager.cleanupSession(info.id);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "session.compacted": {
|
|
111
|
+
const sessionID = extractSessionID(properties);
|
|
112
|
+
if (sessionID) {
|
|
113
|
+
manager.clearCompactionInFlight(sessionID);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "message.part.delta":
|
|
119
|
+
case "session.diff": {
|
|
120
|
+
const sessionID = extractSessionID(properties);
|
|
121
|
+
if (sessionID) {
|
|
122
|
+
manager.recordFirstToken(sessionID);
|
|
123
|
+
manager.clearAwaitingResult(sessionID);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "session.error": {
|
|
129
|
+
const sessionID =
|
|
130
|
+
typeof properties.sessionID === "string" ? properties.sessionID : undefined;
|
|
131
|
+
if (!sessionID) return;
|
|
132
|
+
|
|
133
|
+
const error = properties.error;
|
|
134
|
+
const modelStr = typeof properties.model === "string" ? properties.model : undefined;
|
|
135
|
+
|
|
136
|
+
await handleFallbackError(manager, sdk, config, sessionID, error, modelStr);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "message.updated": {
|
|
141
|
+
const info = properties.info as
|
|
142
|
+
| { sessionID?: string; error?: unknown; model?: string }
|
|
143
|
+
| undefined;
|
|
144
|
+
if (!info?.sessionID || !info.error) return;
|
|
145
|
+
|
|
146
|
+
const modelStr = typeof info.model === "string" ? info.model : undefined;
|
|
147
|
+
await handleFallbackError(manager, sdk, config, info.sessionID, info.error, modelStr);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
// Unknown event type -- ignore
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Shared fallback error handling logic used by both session.error and message.updated.
|
|
160
|
+
*/
|
|
161
|
+
async function handleFallbackError(
|
|
162
|
+
manager: FallbackManager,
|
|
163
|
+
sdk: SdkOperations,
|
|
164
|
+
config: FallbackConfig,
|
|
165
|
+
sessionID: string,
|
|
166
|
+
error: unknown,
|
|
167
|
+
modelStr?: string,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
// All guards (self-abort, stale, retryable, lock) are inside manager.handleError
|
|
170
|
+
const plan = manager.handleError(sessionID, error, modelStr);
|
|
171
|
+
if (!plan) return;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Record self-abort before aborting (Pitfall 2)
|
|
175
|
+
manager.recordSelfAbort(sessionID);
|
|
176
|
+
|
|
177
|
+
// Abort current request
|
|
178
|
+
await sdk.abortSession(sessionID);
|
|
179
|
+
|
|
180
|
+
// Session may have been cleaned up during await — verify before continuing
|
|
181
|
+
if (!manager.getSessionState(sessionID)) {
|
|
182
|
+
manager.releaseRetryLock(sessionID);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get messages for replay
|
|
187
|
+
const messages = await sdk.getSessionMessages(sessionID);
|
|
188
|
+
|
|
189
|
+
// Session existence check after second await
|
|
190
|
+
if (!manager.getSessionState(sessionID)) {
|
|
191
|
+
manager.releaseRetryLock(sessionID);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get current state for attempt-based degradation
|
|
196
|
+
const state = manager.getSessionState(sessionID);
|
|
197
|
+
const attemptCount = state?.attemptCount ?? 0;
|
|
198
|
+
const { parts: replayedParts } = replayWithDegradation(messages, attemptCount);
|
|
199
|
+
|
|
200
|
+
// Commit fallback state — abort dispatch if commit fails (stale plan)
|
|
201
|
+
const committed = manager.commitAndUpdateState(sessionID, plan);
|
|
202
|
+
if (!committed) {
|
|
203
|
+
manager.releaseRetryLock(sessionID);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Parse the new model for the SDK call
|
|
208
|
+
const parsedModel = parseModelString(plan.newModel);
|
|
209
|
+
if (parsedModel) {
|
|
210
|
+
// Notify user if enabled
|
|
211
|
+
if (config.notifyOnFallback) {
|
|
212
|
+
await sdk
|
|
213
|
+
.showToast(
|
|
214
|
+
"Model Fallback",
|
|
215
|
+
`Switching from ${plan.failedModel} to ${plan.newModel}: ${plan.reason}`,
|
|
216
|
+
"warning",
|
|
217
|
+
)
|
|
218
|
+
.catch(() => {
|
|
219
|
+
// Best-effort notification
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Dispatch replay with fallback model
|
|
224
|
+
await sdk.promptAsync(sessionID, parsedModel, replayedParts);
|
|
225
|
+
// Mark awaiting result inside dispatch block — only when prompt was sent
|
|
226
|
+
manager.markAwaitingResult(sessionID);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Release lock after dispatch (or skip if model unparseable)
|
|
230
|
+
manager.releaseRetryLock(sessionID);
|
|
231
|
+
} catch {
|
|
232
|
+
// On failure, release the lock to allow future retries
|
|
233
|
+
manager.releaseRetryLock(sessionID);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const fallbackConfigSchema = z.object({
|
|
4
|
+
enabled: z.boolean().default(true),
|
|
5
|
+
retryOnErrors: z.array(z.number()).default([401, 402, 429, 500, 502, 503, 504]),
|
|
6
|
+
retryableErrorPatterns: z.array(z.string().max(256)).max(50).default([]),
|
|
7
|
+
maxFallbackAttempts: z.number().min(1).max(100).default(10),
|
|
8
|
+
cooldownSeconds: z.number().min(1).max(3600).default(60),
|
|
9
|
+
timeoutSeconds: z.number().min(0).max(300).default(30),
|
|
10
|
+
notifyOnFallback: z.boolean().default(true),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type FallbackConfig = z.infer<typeof fallbackConfigSchema>;
|
|
14
|
+
|
|
15
|
+
// Pre-compute defaults for Zod v4 nested default compatibility
|
|
16
|
+
export const fallbackDefaults = fallbackConfigSchema.parse({});
|