@zq-silk/yui 0.13.8 → 0.13.10
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/README.md +9 -8
- package/dist/cli/commandCatalog.js +21 -2
- package/dist/cli.js +42 -13
- package/dist/commands/agentCommands.js +1 -1
- package/dist/commands/configCommands.js +1 -86
- package/dist/commands/executionAuditCommands.js +17 -16
- package/dist/commands/globalRoleCommands.js +4 -4
- package/dist/commands/sessionCommands.js +2 -6
- package/dist/commands/taskActor.js +1 -2
- package/dist/commands/taskCommands.js +92 -9
- package/dist/commands/taskRoleRuntimeStatus.js +3 -3
- package/dist/config/configCatalog.js +1 -6
- package/dist/config/yuiConfig.js +0 -79
- package/dist/controller/clientRuntime.js +40 -3
- package/dist/controller/controller.js +29 -52
- package/dist/controller/fileSchedulerStoreAdapter.js +305 -1056
- package/dist/controller/runtime.js +12 -5
- package/dist/controller/runtimeHookRunFence.js +3 -9
- package/dist/controller/runtimeLaunchCoordinator.js +44 -67
- package/dist/controller/structuredProviderObservation.js +18 -5
- package/dist/coordination/workMailbox.js +4 -4
- package/dist/execution/executionHealth.js +1 -1
- package/dist/executor/agentExecutor.js +92 -68
- package/dist/executor/executorRegistry.js +8 -20
- package/dist/executor/fileRoleLaunchPlanner.js +24 -90
- package/dist/executor/turnCompletion.js +5 -5
- package/dist/lifecycle/exactRunTerminalization.js +1 -1
- package/dist/observability/executionAudit.js +40 -94
- package/dist/operator/operatorSessionHistory.js +7 -5
- package/dist/role/role.js +1 -1
- package/dist/run/agentRun.js +4 -54
- package/dist/runtime/agentDriver.js +2 -0
- package/dist/runtime/agentError.js +114 -0
- package/dist/runtime/agentHost.js +55 -82
- package/dist/runtime/builtinAgentDrivers.js +21 -9
- package/dist/runtime/builtinAgentErrorMappers.js +150 -0
- package/dist/runtime/exactControlPlane.js +6 -12
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/launchBroker.js +5 -19
- package/dist/runtime/lifecycleReservation.js +20 -4
- package/dist/runtime/providerRuntimeIdentity.js +3 -2
- package/dist/runtime/runtimeBinding.js +0 -27
- package/dist/runtime/runtimeObservation.js +7 -16
- package/dist/runtime/runtimeSessionCandidate.js +3 -10
- package/dist/runtime/sessionLaunchRequest.js +1 -2
- package/dist/runtime/sessionReconciliation.js +2 -2
- package/dist/runtime/structuredProviderHost.js +44 -79
- package/dist/runtime/taskRuntimeIsolation.js +0 -7
- package/dist/runtime/tmuxAdapters.js +6 -49
- package/dist/scheduler/activeRoleRunDelivery.js +218 -168
- package/dist/scheduler/leaderWakeupProcessor.js +126 -86
- package/dist/scheduler/roleRunLiveness.js +4 -1
- package/dist/scheduler/roleRunStall.js +7 -11
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/migration/productionRegistry.js +332 -0
- package/dist/storage/sqliteSchema.js +54 -2
- package/dist/storage/sqliteStore.js +11 -44
- package/dist/storage/taskStore.js +7 -35
- package/dist/storage/upgrade/sqliteStateMigration.js +17 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +30 -8
- package/skills/yui-operator/SKILL.md +14 -6
- package/skills/yui-runtime/SKILL.md +6 -4
- package/dist/lifecycle/providerErrorClass.js +0 -152
- package/dist/run/providerRetry.js +0 -226
- package/dist/run/providerRetryConfig.js +0 -27
- package/dist/runtime/providerErrorCodes.js +0 -278
- package/dist/runtime/providerRecoveryDecision.js +0 -55
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, MAX_PROVIDER_RETRY_ATTEMPTS } from "../config/yuiConfig.js";
|
|
3
|
-
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
4
|
-
export const PROVIDER_RETRY_DELAYS_MS = Object.freeze(DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS.map((seconds) => seconds * 1_000));
|
|
5
|
-
export const PROVIDER_RETRY_EPISODE_WINDOW_MS = DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS * 1_000;
|
|
6
|
-
export function nextProviderRetryDelayMs(retryIndex, delaysMs = PROVIDER_RETRY_DELAYS_MS) {
|
|
7
|
-
if (!Number.isSafeInteger(retryIndex)
|
|
8
|
-
|| retryIndex < 1
|
|
9
|
-
|| retryIndex > delaysMs.length) {
|
|
10
|
-
throw new Error(`Provider retry index is out of range: ${String(retryIndex)}.`);
|
|
11
|
-
}
|
|
12
|
-
return delaysMs[retryIndex - 1];
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* True when the retry lineage has used its total wall-clock budget. The
|
|
16
|
-
* budget is measured from the first classified failure, so repeated failures
|
|
17
|
-
* never extend it.
|
|
18
|
-
*/
|
|
19
|
-
export function providerRetryBudgetExhausted(value, now, maxWindowMs = PROVIDER_RETRY_EPISODE_WINDOW_MS) {
|
|
20
|
-
if (!Number.isSafeInteger(maxWindowMs) || maxWindowMs <= 0) {
|
|
21
|
-
throw new Error(`Provider retry max window must be a positive integer: ${String(maxWindowMs)}.`);
|
|
22
|
-
}
|
|
23
|
-
return Math.min(Date.parse(value.episodeDeadlineAt), Date.parse(value.firstFailureAt) + maxWindowMs) <= now.getTime();
|
|
24
|
-
}
|
|
25
|
-
export function validateAgentRunProviderRetry(value) {
|
|
26
|
-
if (value.schemaVersion !== 2) {
|
|
27
|
-
throw new Error("Agent run providerRetry must use schemaVersion 2.");
|
|
28
|
-
}
|
|
29
|
-
if (!["scheduled", "dispatching", "awaiting-progress", "blocked"].includes(value.state)) {
|
|
30
|
-
throw new Error("Agent run providerRetry state is invalid.");
|
|
31
|
-
}
|
|
32
|
-
requireIdentity(value.episodeId, "Agent run providerRetry episodeId");
|
|
33
|
-
requireIdentity(value.failureEventId, "Agent run providerRetry failureEventId");
|
|
34
|
-
if (value.policyVersion !== 1)
|
|
35
|
-
throw new Error("Provider retry policy version is invalid.");
|
|
36
|
-
if (!Number.isSafeInteger(value.consecutiveFailures) || value.consecutiveFailures < 1) {
|
|
37
|
-
throw new Error("Agent run providerRetry consecutiveFailures must be positive.");
|
|
38
|
-
}
|
|
39
|
-
if (!Number.isSafeInteger(value.dispatchedRetries)
|
|
40
|
-
|| value.dispatchedRetries < 0
|
|
41
|
-
|| value.dispatchedRetries > value.maxRetries) {
|
|
42
|
-
throw new Error("Agent run providerRetry dispatchedRetries is invalid.");
|
|
43
|
-
}
|
|
44
|
-
if (!Number.isSafeInteger(value.maxRetries)
|
|
45
|
-
|| value.maxRetries < 1
|
|
46
|
-
|| value.maxRetries > MAX_PROVIDER_RETRY_ATTEMPTS) {
|
|
47
|
-
throw new Error("Agent run providerRetry maxRetries is invalid.");
|
|
48
|
-
}
|
|
49
|
-
requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
|
|
50
|
-
requireTimestamp(value.lastFailureAt, "Agent run providerRetry lastFailureAt");
|
|
51
|
-
requireTimestamp(value.episodeDeadlineAt, "Agent run providerRetry episodeDeadlineAt");
|
|
52
|
-
if (Date.parse(value.lastFailureAt) < Date.parse(value.firstFailureAt)) {
|
|
53
|
-
throw new Error("Agent run providerRetry lastFailureAt precedes firstFailureAt.");
|
|
54
|
-
}
|
|
55
|
-
if (Date.parse(value.episodeDeadlineAt) <= Date.parse(value.firstFailureAt)) {
|
|
56
|
-
throw new Error("Agent run providerRetry deadline must follow firstFailureAt.");
|
|
57
|
-
}
|
|
58
|
-
if ((value.state === "scheduled") !== (value.nextAttemptAt !== undefined)) {
|
|
59
|
-
throw new Error("Only a scheduled providerRetry may carry nextAttemptAt.");
|
|
60
|
-
}
|
|
61
|
-
if (value.nextAttemptAt !== undefined) {
|
|
62
|
-
requireTimestamp(value.nextAttemptAt, "Agent run providerRetry nextAttemptAt");
|
|
63
|
-
if (Date.parse(value.nextAttemptAt) > Date.parse(value.episodeDeadlineAt)) {
|
|
64
|
-
throw new Error("Agent run providerRetry next attempt exceeds its episode deadline.");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if ((value.state === "dispatching" || value.state === "awaiting-progress")
|
|
68
|
-
!== (value.lastRetryReceiptId !== undefined)) {
|
|
69
|
-
throw new Error("An in-flight providerRetry requires one retry receipt identity.");
|
|
70
|
-
}
|
|
71
|
-
if (value.lastRetryReceiptId !== undefined) {
|
|
72
|
-
requireIdentity(value.lastRetryReceiptId, "Agent run providerRetry receipt id");
|
|
73
|
-
}
|
|
74
|
-
if (value.launchId !== undefined)
|
|
75
|
-
requireIdentity(value.launchId, "Agent run providerRetry launchId");
|
|
76
|
-
if (value.nativeSessionId !== undefined) {
|
|
77
|
-
requireIdentity(value.nativeSessionId, "Agent run providerRetry nativeSessionId");
|
|
78
|
-
}
|
|
79
|
-
if (value.failedNativeTurnId !== undefined) {
|
|
80
|
-
requireIdentity(value.failedNativeTurnId, "Agent run providerRetry failed native Turn id");
|
|
81
|
-
}
|
|
82
|
-
requireText(value.lastErrorSummary, "Agent run providerRetry lastErrorSummary");
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
/** Advance one failure episode without ever changing the native Session. */
|
|
86
|
-
export function scheduleProviderRetry(previous, input, now, policy = {
|
|
87
|
-
delaysMs: PROVIDER_RETRY_DELAYS_MS,
|
|
88
|
-
maxWindowMs: PROVIDER_RETRY_EPISODE_WINDOW_MS
|
|
89
|
-
}) {
|
|
90
|
-
validateRetrySchedulePolicy(policy);
|
|
91
|
-
const at = now.toISOString();
|
|
92
|
-
const firstFailureAt = previous?.firstFailureAt ?? at;
|
|
93
|
-
const episodeDeadlineAt = previous?.episodeDeadlineAt
|
|
94
|
-
?? new Date(now.getTime() + policy.maxWindowMs).toISOString();
|
|
95
|
-
if (now.getTime() >= Date.parse(episodeDeadlineAt)) {
|
|
96
|
-
return Object.freeze({ outcome: "exhausted", reason: "window" });
|
|
97
|
-
}
|
|
98
|
-
const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
|
|
99
|
-
const dispatchedRetries = previous?.dispatchedRetries ?? 0;
|
|
100
|
-
const schedule = input.scheduleNextAttempt ?? true;
|
|
101
|
-
if (schedule && dispatchedRetries >= policy.delaysMs.length) {
|
|
102
|
-
return Object.freeze({ outcome: "exhausted", reason: "attempts" });
|
|
103
|
-
}
|
|
104
|
-
const state = schedule ? "scheduled" : "blocked";
|
|
105
|
-
const retryAfterMs = input.retryAfterMs;
|
|
106
|
-
if (retryAfterMs !== undefined && (!Number.isSafeInteger(retryAfterMs) || retryAfterMs <= 0)) {
|
|
107
|
-
throw new Error("Provider retry Retry-After must be a positive safe integer.");
|
|
108
|
-
}
|
|
109
|
-
const delayMs = schedule
|
|
110
|
-
? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1, policy.delaysMs), retryAfterMs ?? 0)
|
|
111
|
-
: undefined;
|
|
112
|
-
const nextAttemptAt = delayMs === undefined
|
|
113
|
-
? undefined
|
|
114
|
-
: new Date(now.getTime() + delayMs).toISOString();
|
|
115
|
-
if (nextAttemptAt !== undefined && Date.parse(nextAttemptAt) > Date.parse(episodeDeadlineAt)) {
|
|
116
|
-
return Object.freeze({
|
|
117
|
-
outcome: "exhausted",
|
|
118
|
-
reason: retryAfterMs === undefined ? "window" : "retry-after-window"
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
const retry = validateAgentRunProviderRetry({
|
|
122
|
-
schemaVersion: 2,
|
|
123
|
-
episodeId: previous?.episodeId ?? createHash("sha256")
|
|
124
|
-
.update(`${firstFailureAt}\0${input.nativeSessionId ?? "unknown"}\0${input.launchId ?? "unknown"}`)
|
|
125
|
-
.digest("hex"),
|
|
126
|
-
failureEventId: requireIdentity(input.failureEventId, "Provider failure event id"),
|
|
127
|
-
policyVersion: 1,
|
|
128
|
-
state,
|
|
129
|
-
errorClass: input.errorClass,
|
|
130
|
-
consecutiveFailures,
|
|
131
|
-
dispatchedRetries,
|
|
132
|
-
maxRetries: policy.delaysMs.length,
|
|
133
|
-
firstFailureAt,
|
|
134
|
-
lastFailureAt: at,
|
|
135
|
-
episodeDeadlineAt,
|
|
136
|
-
...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
|
|
137
|
-
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
138
|
-
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
139
|
-
...(input.failedNativeTurnId === undefined
|
|
140
|
-
? {}
|
|
141
|
-
: { failedNativeTurnId: input.failedNativeTurnId }),
|
|
142
|
-
lastErrorSummary: input.lastErrorSummary
|
|
143
|
-
});
|
|
144
|
-
return Object.freeze({ outcome: schedule ? "scheduled" : "blocked", retry });
|
|
145
|
-
}
|
|
146
|
-
function validateRetrySchedulePolicy(policy) {
|
|
147
|
-
if (!Number.isSafeInteger(policy.maxWindowMs) || policy.maxWindowMs < 1) {
|
|
148
|
-
throw new Error("Provider retry max window must be a positive safe integer.");
|
|
149
|
-
}
|
|
150
|
-
if (policy.delaysMs.length < 1 || policy.delaysMs.length > MAX_PROVIDER_RETRY_ATTEMPTS
|
|
151
|
-
|| policy.delaysMs.some((delay) => !Number.isSafeInteger(delay) || delay < 1)) {
|
|
152
|
-
throw new Error(`Provider retry delay schedule must contain 1-${MAX_PROVIDER_RETRY_ATTEMPTS} positive safe integers.`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/** Mark that one short continuation request was dispatched and now awaits any correlated progress. */
|
|
156
|
-
export function prepareProviderRetryDispatch(value, receiptId, now) {
|
|
157
|
-
if (value.state !== "scheduled" || value.nextAttemptAt === undefined)
|
|
158
|
-
return value;
|
|
159
|
-
if (now.getTime() > Date.parse(value.episodeDeadlineAt)) {
|
|
160
|
-
throw new Error("Provider retry episode expired before dispatch.");
|
|
161
|
-
}
|
|
162
|
-
const { nextAttemptAt: _nextAttemptAt, ...rest } = value;
|
|
163
|
-
return validateAgentRunProviderRetry({
|
|
164
|
-
...rest,
|
|
165
|
-
state: "dispatching",
|
|
166
|
-
lastRetryReceiptId: requireIdentity(receiptId, "Provider retry receipt id")
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
export function markProviderRetryDispatched(value) {
|
|
170
|
-
if (value.state !== "dispatching")
|
|
171
|
-
return value;
|
|
172
|
-
return validateAgentRunProviderRetry({
|
|
173
|
-
...value,
|
|
174
|
-
state: "awaiting-progress",
|
|
175
|
-
dispatchedRetries: value.dispatchedRetries + 1
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
export function providerRetryIsDue(value, now) {
|
|
179
|
-
return value.state === "scheduled"
|
|
180
|
-
&& value.nextAttemptAt !== undefined
|
|
181
|
-
&& Date.parse(value.nextAttemptAt) <= now.getTime();
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Next Controller wake for an active automatic retry episode. Scheduled
|
|
185
|
-
* retries wake to dispatch; an in-flight retry wakes only at the episode
|
|
186
|
-
* deadline so silence cannot strand the Run forever. Blocked states are
|
|
187
|
-
* intentionally excluded because they require native/user evidence rather
|
|
188
|
-
* than an automatic lifecycle transition.
|
|
189
|
-
*/
|
|
190
|
-
export function providerRetryWakeAt(value) {
|
|
191
|
-
if (value.state === "scheduled")
|
|
192
|
-
return value.nextAttemptAt ?? null;
|
|
193
|
-
if (value.state === "dispatching" || value.state === "awaiting-progress") {
|
|
194
|
-
return value.episodeDeadlineAt;
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
/** Delay a control-plane recovery gap without changing either failure counter. */
|
|
199
|
-
export function deferProviderRetry(value, now) {
|
|
200
|
-
const deadline = Date.parse(value.episodeDeadlineAt);
|
|
201
|
-
if (now.getTime() >= deadline)
|
|
202
|
-
return null;
|
|
203
|
-
const { lastRetryReceiptId: _receipt, nextAttemptAt: _next, ...rest } = value;
|
|
204
|
-
return validateAgentRunProviderRetry({
|
|
205
|
-
...rest,
|
|
206
|
-
state: "scheduled",
|
|
207
|
-
nextAttemptAt: new Date(Math.min(now.getTime() + 15_000, deadline)).toISOString()
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
/** Short recovery instruction; it cannot repeat the Task Assignment body. */
|
|
211
|
-
export function serializeProviderRetryEnvelope(input) {
|
|
212
|
-
validateAgentRunProviderRetry(input.retry);
|
|
213
|
-
const retryOrdinal = input.retry.state === "dispatching"
|
|
214
|
-
? input.retry.dispatchedRetries + 1
|
|
215
|
-
: input.retry.dispatchedRetries;
|
|
216
|
-
return [
|
|
217
|
-
"Yui managed in-Session continuation retry.",
|
|
218
|
-
`task=${requireIdentity(input.taskId, "Provider retry task id")} run=${requireIdentity(input.runId, "Provider retry run id")} role=${requireIdentity(input.roleName, "Provider retry role")}`,
|
|
219
|
-
`episode=${input.retry.episodeId} retry=${retryOrdinal}/${input.retry.maxRetries} receipt=${input.retry.lastRetryReceiptId ?? "pending"}`,
|
|
220
|
-
`failureEvent=${input.retry.failureEventId}`,
|
|
221
|
-
...(input.retry.failedNativeTurnId === undefined
|
|
222
|
-
? []
|
|
223
|
-
: [`retryOfTurn=${input.retry.failedNativeTurnId}`]),
|
|
224
|
-
"Continue the existing native conversation from its latest accepted state. Do not repeat completed work; load exact Run deltas if needed."
|
|
225
|
-
].join("\n");
|
|
226
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode } from "../config/yuiConfig.js";
|
|
2
|
-
/**
|
|
3
|
-
* Resolves the retry flags from the durable Yui config. Homes without the
|
|
4
|
-
* fields get the safe defaults: enforce mode, all supported adapters, receipt
|
|
5
|
-
* replay on, 10-minute budget.
|
|
6
|
-
*/
|
|
7
|
-
export function providerRetryConfig(config) {
|
|
8
|
-
const mode = resolveProviderRetryMode(config.providerRetryMode);
|
|
9
|
-
const adapters = resolveProviderRetryAdapters(config.providerRetryAdapters);
|
|
10
|
-
return {
|
|
11
|
-
mode: adapters.length === 0 ? "off" : mode,
|
|
12
|
-
adapters,
|
|
13
|
-
delaysMs: resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds)
|
|
14
|
-
.map((seconds) => seconds * 1_000),
|
|
15
|
-
maxWindowMs: resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds)
|
|
16
|
-
* 1_000
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/** Whether the adapter has in-place retry enabled in the given mode. */
|
|
20
|
-
export function providerRetryEnabledForAdapter(config, adapterId, mode) {
|
|
21
|
-
return config.mode === mode
|
|
22
|
-
&& providerRetryAdapterEnabled(config, adapterId);
|
|
23
|
-
}
|
|
24
|
-
/** Default admission is capability-driven, not a hard-coded Provider list. */
|
|
25
|
-
export function providerRetryAdapterEnabled(config, adapterId) {
|
|
26
|
-
return config.adapters === "all-capable" || config.adapters.includes(adapterId);
|
|
27
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider-neutral structured error taxonomy.
|
|
3
|
-
*
|
|
4
|
-
* Each Agent Driver parses its own Provider's raw failure text into one of
|
|
5
|
-
* these codes at the driver boundary. The retry classifier then maps codes to
|
|
6
|
-
* Yui error classes by lookup — no Provider-specific regex lives in the
|
|
7
|
-
* classifier. Text matching remains only as a fallback for drivers that
|
|
8
|
-
* cannot yet produce a structured code.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Maps each structured code to its Yui error class. This is the single
|
|
12
|
-
* authoritative lookup that replaces regex matching in the classifier.
|
|
13
|
-
*/
|
|
14
|
-
export const PROVIDER_ERROR_CODE_CLASS = Object.freeze({
|
|
15
|
-
// Stream/transport → transport-uncertain (delivery may have happened)
|
|
16
|
-
"stream-internal-error": "transport-uncertain",
|
|
17
|
-
"stream-protocol-error": "transport-uncertain",
|
|
18
|
-
"stream-error": "transport-uncertain",
|
|
19
|
-
"connection-reset": "transport-uncertain",
|
|
20
|
-
"connection-lost": "transport-uncertain",
|
|
21
|
-
timeout: "transport-uncertain",
|
|
22
|
-
// HTTP 5xx / capacity → transient-provider
|
|
23
|
-
"http-5xx": "transient-provider",
|
|
24
|
-
overloaded: "transient-provider",
|
|
25
|
-
"server-error": "transient-provider",
|
|
26
|
-
// HTTP 429 → transient-provider (retryable with backoff)
|
|
27
|
-
"http-429": "transient-provider",
|
|
28
|
-
// HTTP 4xx → invalid-request (non-retryable)
|
|
29
|
-
"http-4xx": "invalid-request",
|
|
30
|
-
// Policy
|
|
31
|
-
"policy-denied": "policy-denied",
|
|
32
|
-
// Session
|
|
33
|
-
"session-not-found": "session-dead",
|
|
34
|
-
"session-expired": "session-dead",
|
|
35
|
-
"session-ended": "session-dead",
|
|
36
|
-
"process-exited": "session-dead",
|
|
37
|
-
// Request
|
|
38
|
-
"invalid-request": "invalid-request",
|
|
39
|
-
unknown: "unclassified"
|
|
40
|
-
});
|
|
41
|
-
/** Whether a structured code is retryable in place. */
|
|
42
|
-
export function isRetryableErrorCode(code) {
|
|
43
|
-
const cls = PROVIDER_ERROR_CODE_CLASS[code];
|
|
44
|
-
return cls === "transient-provider" || cls === "transport-uncertain";
|
|
45
|
-
}
|
|
46
|
-
// ── Claude Code driver ──────────────────────────────────────────────────
|
|
47
|
-
/**
|
|
48
|
-
* Parses a Claude Code StopFailure `error` string into a structured code.
|
|
49
|
-
*
|
|
50
|
-
* Claude sends structured API error codes (server_error, overloaded_error,
|
|
51
|
-
* rate_limit_error, etc.) as the `error` field, and raw transport text
|
|
52
|
-
* ("stream error: stream ID …; INTERNAL_ERROR") in error_details or the
|
|
53
|
-
* CLI's own output. This function handles both.
|
|
54
|
-
*/
|
|
55
|
-
export function parseClaudeError(error, details) {
|
|
56
|
-
const text = [error, details]
|
|
57
|
-
.filter((v) => typeof v === "string" && v.length > 0)
|
|
58
|
-
.join("\n");
|
|
59
|
-
// ── Structured Claude API error codes ──────────────────────────────
|
|
60
|
-
// Claude's StopFailure hook sends these as the `error` field.
|
|
61
|
-
if (/^server_error$/iu.test(text)) {
|
|
62
|
-
return { code: "server-error", raw: error };
|
|
63
|
-
}
|
|
64
|
-
if (/^overloaded_error$/iu.test(text)) {
|
|
65
|
-
return { code: "overloaded", raw: error };
|
|
66
|
-
}
|
|
67
|
-
if (/^rate_limit_error$/iu.test(text)) {
|
|
68
|
-
return { code: "http-429", raw: error };
|
|
69
|
-
}
|
|
70
|
-
if (/^invalid_request_error$/iu.test(text)) {
|
|
71
|
-
return { code: "invalid-request", raw: error };
|
|
72
|
-
}
|
|
73
|
-
if (/^(authentication_error|permission_error)$/iu.test(text)) {
|
|
74
|
-
return { code: "policy-denied", raw: error };
|
|
75
|
-
}
|
|
76
|
-
if (/^not_found_error$/iu.test(text)) {
|
|
77
|
-
return { code: "session-not-found", raw: error };
|
|
78
|
-
}
|
|
79
|
-
if (/^api_error$/iu.test(text)) {
|
|
80
|
-
return { code: "server-error", raw: error };
|
|
81
|
-
}
|
|
82
|
-
// Claude CLI structured error codes (e.g. "[claude-code:unrecognized_model]")
|
|
83
|
-
if (/\[claude-code:(unrecognized_model|invalid_model|model_not_found)\]/iu.test(text)) {
|
|
84
|
-
return { code: "invalid-request", raw: error };
|
|
85
|
-
}
|
|
86
|
-
// HTTP/2 RST_STREAM (the Task-27 failure mode)
|
|
87
|
-
if (/stream error:.*INTERNAL_ERROR/iu.test(text)) {
|
|
88
|
-
return { code: "stream-internal-error", raw: error };
|
|
89
|
-
}
|
|
90
|
-
if (/stream error:.*PROTOCOL_ERROR/iu.test(text)) {
|
|
91
|
-
return { code: "stream-protocol-error", raw: error };
|
|
92
|
-
}
|
|
93
|
-
if (/stream error/iu.test(text)) {
|
|
94
|
-
return { code: "stream-error", raw: error };
|
|
95
|
-
}
|
|
96
|
-
// "Server error mid-response" and similar
|
|
97
|
-
if (/server[\s_-]?error/iu.test(text)) {
|
|
98
|
-
return { code: "server-error", raw: error };
|
|
99
|
-
}
|
|
100
|
-
// HTTP status codes
|
|
101
|
-
if (/\b429\b/u.test(text))
|
|
102
|
-
return { code: "http-429", raw: error };
|
|
103
|
-
if (/\b40[0-9]\b/u.test(text))
|
|
104
|
-
return { code: "http-4xx", raw: error };
|
|
105
|
-
if (/\b50[024]\b/u.test(text))
|
|
106
|
-
return { code: "http-5xx", raw: error };
|
|
107
|
-
// Connection
|
|
108
|
-
if (/connection[\s_-]?reset/iu.test(text))
|
|
109
|
-
return { code: "connection-reset", raw: error };
|
|
110
|
-
if (/connection[\s_-]?lost/iu.test(text))
|
|
111
|
-
return { code: "connection-lost", raw: error };
|
|
112
|
-
if (/econnreset/iu.test(text))
|
|
113
|
-
return { code: "connection-reset", raw: error };
|
|
114
|
-
if (/socket hang up/iu.test(text))
|
|
115
|
-
return { code: "connection-reset", raw: error };
|
|
116
|
-
// Timeout
|
|
117
|
-
if (/timed?[ -]?out/iu.test(text))
|
|
118
|
-
return { code: "timeout", raw: error };
|
|
119
|
-
if (/etimedout/iu.test(text))
|
|
120
|
-
return { code: "timeout", raw: error };
|
|
121
|
-
// Capacity
|
|
122
|
-
if (/overloaded/iu.test(text))
|
|
123
|
-
return { code: "overloaded", raw: error };
|
|
124
|
-
if (/rate[\s_-]?limit/iu.test(text))
|
|
125
|
-
return { code: "http-429", raw: error };
|
|
126
|
-
// Policy
|
|
127
|
-
if (/cyber[_-]?policy/iu.test(text))
|
|
128
|
-
return { code: "policy-denied", raw: error };
|
|
129
|
-
if (/policy[\s_-]?violation/iu.test(text))
|
|
130
|
-
return { code: "policy-denied", raw: error };
|
|
131
|
-
if (/usage[\s_-]?policy/iu.test(text))
|
|
132
|
-
return { code: "policy-denied", raw: error };
|
|
133
|
-
if (/content[\s_-]?policy/iu.test(text))
|
|
134
|
-
return { code: "policy-denied", raw: error };
|
|
135
|
-
if (/safety[\s_-]?policy/iu.test(text))
|
|
136
|
-
return { code: "policy-denied", raw: error };
|
|
137
|
-
// Session lifecycle
|
|
138
|
-
if (/session[\s_-]?not[\s_-]?found/iu.test(text))
|
|
139
|
-
return { code: "session-not-found", raw: error };
|
|
140
|
-
if (/no[\s_-]?such[\s_-]?(session|thread)/iu.test(text))
|
|
141
|
-
return { code: "session-not-found", raw: error };
|
|
142
|
-
if (/thread[\s_-]?not[\s_-]?found/iu.test(text))
|
|
143
|
-
return { code: "session-not-found", raw: error };
|
|
144
|
-
if (/session[\s_-]?(has[\s_-]?)?expired/iu.test(text))
|
|
145
|
-
return { code: "session-expired", raw: error };
|
|
146
|
-
if (/session[\s_-]?(has[\s_-]?)?ended/iu.test(text))
|
|
147
|
-
return { code: "session-ended", raw: error };
|
|
148
|
-
if (/process[\s_-]?exited/iu.test(text))
|
|
149
|
-
return { code: "process-exited", raw: error };
|
|
150
|
-
// Request validity
|
|
151
|
-
if (/invalid[\s_-]?request/iu.test(text))
|
|
152
|
-
return { code: "invalid-request", raw: error };
|
|
153
|
-
if (/validation[\s_-]?error/iu.test(text))
|
|
154
|
-
return { code: "invalid-request", raw: error };
|
|
155
|
-
if (/bad[\s_-]?request/iu.test(text))
|
|
156
|
-
return { code: "http-4xx", raw: error };
|
|
157
|
-
if (/unknown[\s_-]?(flag|tool|argument)/iu.test(text))
|
|
158
|
-
return { code: "invalid-request", raw: error };
|
|
159
|
-
return { code: "unknown", raw: error };
|
|
160
|
-
}
|
|
161
|
-
// ── Codex driver ────────────────────────────────────────────────────────
|
|
162
|
-
/**
|
|
163
|
-
* Parses a Codex CLI failure into a structured code.
|
|
164
|
-
*
|
|
165
|
-
* Codex surfaces errors through process exit, transcript messages, and
|
|
166
|
-
* stream-level failures. Its error formats overlap with Claude's (HTTP/2
|
|
167
|
-
* stream errors, API status codes) but also include Codex-specific patterns.
|
|
168
|
-
*/
|
|
169
|
-
export function parseCodexError(error, details) {
|
|
170
|
-
const text = [error, details]
|
|
171
|
-
.filter((v) => typeof v === "string" && v.length > 0)
|
|
172
|
-
.join("\n");
|
|
173
|
-
// ── Structured API error codes ────────────────────────────────────
|
|
174
|
-
if (/^server_error$/iu.test(text)) {
|
|
175
|
-
return { code: "server-error", raw: error };
|
|
176
|
-
}
|
|
177
|
-
if (/^overloaded_error$/iu.test(text)) {
|
|
178
|
-
return { code: "overloaded", raw: error };
|
|
179
|
-
}
|
|
180
|
-
if (/^rate_limit_error$/iu.test(text)) {
|
|
181
|
-
return { code: "http-429", raw: error };
|
|
182
|
-
}
|
|
183
|
-
if (/^invalid_request_error$/iu.test(text)) {
|
|
184
|
-
return { code: "invalid-request", raw: error };
|
|
185
|
-
}
|
|
186
|
-
if (/^(authentication_error|permission_error)$/iu.test(text)) {
|
|
187
|
-
return { code: "policy-denied", raw: error };
|
|
188
|
-
}
|
|
189
|
-
// Codex CLI structured error codes
|
|
190
|
-
if (/\[codex:(unrecognized_model|invalid_model|model_not_found)\]/iu.test(text)) {
|
|
191
|
-
return { code: "invalid-request", raw: error };
|
|
192
|
-
}
|
|
193
|
-
// Model not supported / invalid model (non-retryable)
|
|
194
|
-
if (/model.*not supported|invalid.*model|model.*not found/iu.test(text)) {
|
|
195
|
-
return { code: "invalid-request", raw: error };
|
|
196
|
-
}
|
|
197
|
-
// Stream disconnected (retryable transport)
|
|
198
|
-
if (/stream disconnected/iu.test(text)) {
|
|
199
|
-
return { code: "stream-error", raw: error };
|
|
200
|
-
}
|
|
201
|
-
// ── Stream / transport ────────────────────────────────────────────
|
|
202
|
-
if (/stream error:.*INTERNAL_ERROR/iu.test(text)) {
|
|
203
|
-
return { code: "stream-internal-error", raw: error };
|
|
204
|
-
}
|
|
205
|
-
if (/stream error:.*PROTOCOL_ERROR/iu.test(text)) {
|
|
206
|
-
return { code: "stream-protocol-error", raw: error };
|
|
207
|
-
}
|
|
208
|
-
if (/stream error/iu.test(text)) {
|
|
209
|
-
return { code: "stream-error", raw: error };
|
|
210
|
-
}
|
|
211
|
-
// ── HTTP status codes ─────────────────────────────────────────────
|
|
212
|
-
if (/\b429\b/u.test(text))
|
|
213
|
-
return { code: "http-429", raw: error };
|
|
214
|
-
if (/\b40[0-9]\b/u.test(text))
|
|
215
|
-
return { code: "http-4xx", raw: error };
|
|
216
|
-
if (/\b50[024]\b/u.test(text))
|
|
217
|
-
return { code: "http-5xx", raw: error };
|
|
218
|
-
// ── Server / capacity ─────────────────────────────────────────────
|
|
219
|
-
if (/server[\s_-]?error/iu.test(text)) {
|
|
220
|
-
return { code: "server-error", raw: error };
|
|
221
|
-
}
|
|
222
|
-
if (/overloaded/iu.test(text))
|
|
223
|
-
return { code: "overloaded", raw: error };
|
|
224
|
-
if (/rate[\s_-]?limit/iu.test(text))
|
|
225
|
-
return { code: "http-429", raw: error };
|
|
226
|
-
if (/bad gateway/iu.test(text))
|
|
227
|
-
return { code: "http-5xx", raw: error };
|
|
228
|
-
if (/gateway timeout/iu.test(text))
|
|
229
|
-
return { code: "timeout", raw: error };
|
|
230
|
-
if (/service unavailable/iu.test(text))
|
|
231
|
-
return { code: "http-5xx", raw: error };
|
|
232
|
-
if (/temporarily unavailable/iu.test(text))
|
|
233
|
-
return { code: "http-5xx", raw: error };
|
|
234
|
-
// ── Connection ────────────────────────────────────────────────────
|
|
235
|
-
if (/connection[\s_-]?reset/iu.test(text))
|
|
236
|
-
return { code: "connection-reset", raw: error };
|
|
237
|
-
if (/connection[\s_-]?lost/iu.test(text))
|
|
238
|
-
return { code: "connection-lost", raw: error };
|
|
239
|
-
if (/econnreset/iu.test(text))
|
|
240
|
-
return { code: "connection-reset", raw: error };
|
|
241
|
-
if (/socket hang up/iu.test(text))
|
|
242
|
-
return { code: "connection-reset", raw: error };
|
|
243
|
-
// ── Timeout ───────────────────────────────────────────────────────
|
|
244
|
-
if (/timed?[ -]?out/iu.test(text))
|
|
245
|
-
return { code: "timeout", raw: error };
|
|
246
|
-
if (/etimedout/iu.test(text))
|
|
247
|
-
return { code: "timeout", raw: error };
|
|
248
|
-
// ── Policy ────────────────────────────────────────────────────────
|
|
249
|
-
if (/cyber[_-]?policy/iu.test(text))
|
|
250
|
-
return { code: "policy-denied", raw: error };
|
|
251
|
-
if (/policy[\s_-]?violation/iu.test(text))
|
|
252
|
-
return { code: "policy-denied", raw: error };
|
|
253
|
-
if (/usage[\s_-]?policy/iu.test(text))
|
|
254
|
-
return { code: "policy-denied", raw: error };
|
|
255
|
-
if (/content[\s_-]?policy/iu.test(text))
|
|
256
|
-
return { code: "policy-denied", raw: error };
|
|
257
|
-
// ── Session ───────────────────────────────────────────────────────
|
|
258
|
-
if (/session[\s_-]?not[\s_-]?found/iu.test(text))
|
|
259
|
-
return { code: "session-not-found", raw: error };
|
|
260
|
-
if (/no[\s_-]?such[\s_-]?(session|thread)/iu.test(text))
|
|
261
|
-
return { code: "session-not-found", raw: error };
|
|
262
|
-
if (/session[\s_-]?(has[\s_-]?)?expired/iu.test(text))
|
|
263
|
-
return { code: "session-expired", raw: error };
|
|
264
|
-
if (/session[\s_-]?(has[\s_-]?)?ended/iu.test(text))
|
|
265
|
-
return { code: "session-ended", raw: error };
|
|
266
|
-
if (/process[\s_-]?exited/iu.test(text))
|
|
267
|
-
return { code: "process-exited", raw: error };
|
|
268
|
-
// ── Request validity ──────────────────────────────────────────────
|
|
269
|
-
if (/invalid[\s_-]?request/iu.test(text))
|
|
270
|
-
return { code: "invalid-request", raw: error };
|
|
271
|
-
if (/validation[\s_-]?error/iu.test(text))
|
|
272
|
-
return { code: "invalid-request", raw: error };
|
|
273
|
-
if (/bad[\s_-]?request/iu.test(text))
|
|
274
|
-
return { code: "http-4xx", raw: error };
|
|
275
|
-
if (/unknown[\s_-]?(flag|tool|argument)/iu.test(text))
|
|
276
|
-
return { code: "invalid-request", raw: error };
|
|
277
|
-
return { code: "unknown", raw: error };
|
|
278
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { currentProviderActivation, currentProviderConversation, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
|
|
2
|
-
/**
|
|
3
|
-
* Exact tri-state recovery policy. Unknown is a terminal decision for the
|
|
4
|
-
* automatic recovery attempt, not permission to create another Conversation.
|
|
5
|
-
*/
|
|
6
|
-
export function decideProviderRecovery(input) {
|
|
7
|
-
const binding = validateProviderRuntimeBinding(input.binding);
|
|
8
|
-
const conversation = currentProviderConversation(binding);
|
|
9
|
-
if (input.probe.conversationId !== conversation.conversationId) {
|
|
10
|
-
throw new Error("Provider recovery probe targets a different Conversation.");
|
|
11
|
-
}
|
|
12
|
-
if (input.probe.state === "unknown") {
|
|
13
|
-
return {
|
|
14
|
-
action: "attention",
|
|
15
|
-
conversationId: conversation.conversationId,
|
|
16
|
-
reason: "Provider Conversation existence is unknown; replacement is fenced."
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
if (input.probe.state === "exists") {
|
|
20
|
-
if (input.probe.activeTurnId !== undefined) {
|
|
21
|
-
return {
|
|
22
|
-
action: "observe-active-turn",
|
|
23
|
-
conversationId: conversation.conversationId,
|
|
24
|
-
turnId: input.probe.activeTurnId
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
if (input.unsettledInputDelivery || providerTurnIsUnsettled(binding)) {
|
|
28
|
-
return {
|
|
29
|
-
action: "attention",
|
|
30
|
-
conversationId: conversation.conversationId,
|
|
31
|
-
reason: "Provider Conversation exists but prior input delivery is still unsettled."
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return { action: "resume", conversationId: conversation.conversationId };
|
|
35
|
-
}
|
|
36
|
-
if (input.unsettledInputDelivery || providerTurnIsUnsettled(binding)) {
|
|
37
|
-
return {
|
|
38
|
-
action: "attention",
|
|
39
|
-
conversationId: conversation.conversationId,
|
|
40
|
-
reason: "Provider Conversation is missing but input delivery remains unsettled."
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (currentProviderActivation(binding) !== null || binding.authority.owner !== "none") {
|
|
44
|
-
return {
|
|
45
|
-
action: "attention",
|
|
46
|
-
conversationId: conversation.conversationId,
|
|
47
|
-
reason: "Provider Conversation is missing but its Activation writer has not ended."
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return { action: "restart-run", conversationId: conversation.conversationId };
|
|
51
|
-
}
|
|
52
|
-
function providerTurnIsUnsettled(binding) {
|
|
53
|
-
return binding.turn !== null
|
|
54
|
-
&& ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status);
|
|
55
|
-
}
|