@centerforagenticai/pi-multi-account 0.1.1
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/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
AssistantMessageEventStream,
|
|
4
|
+
SimpleStreamOptions,
|
|
5
|
+
} from "@earendil-works/pi-ai";
|
|
6
|
+
import { isManagedFamily } from "./config.js";
|
|
7
|
+
import type { MultiAccountConfig } from "./config.js";
|
|
8
|
+
import {
|
|
9
|
+
AcceptedOutputError,
|
|
10
|
+
createAcceptedOutputStream,
|
|
11
|
+
type AcceptedOutputErrorCode,
|
|
12
|
+
} from "./recovery-output.js";
|
|
13
|
+
import type { RecoveryCandidate } from "./recovery-plan.js";
|
|
14
|
+
|
|
15
|
+
/** A request-owned timer. Cancelling it must prevent its callback from running. */
|
|
16
|
+
export interface RecoveryTimer {
|
|
17
|
+
cancel(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Monotonic time and timers are injected so recovery never depends on wall-clock sleeps. */
|
|
21
|
+
export interface RecoveryClock {
|
|
22
|
+
now(): number;
|
|
23
|
+
setTimer(delayMs: number, callback: () => void): RecoveryTimer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type RecoveryPreDispatchDecision =
|
|
27
|
+
| { readonly status: "eligible" }
|
|
28
|
+
| { readonly status: "skip" }
|
|
29
|
+
| {
|
|
30
|
+
readonly status: "terminate";
|
|
31
|
+
readonly reason: "malformed-config" | "callback-failure";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RecoveryRetrySafety =
|
|
35
|
+
| {
|
|
36
|
+
readonly status: "retryable";
|
|
37
|
+
readonly reason: "generation-only-incomplete" | "definitively-rejected";
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
readonly status: "unsafe";
|
|
41
|
+
readonly reason:
|
|
42
|
+
| "uncertain-external-effects"
|
|
43
|
+
| "completed-tool"
|
|
44
|
+
| "completed-call"
|
|
45
|
+
| "agent-run";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Request options after recovery has replaced the provider's inner retry allowance. */
|
|
49
|
+
export type RecoveryBoundStreamOptions = SimpleStreamOptions & {
|
|
50
|
+
readonly maxRetries: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A pre-send reservation is an upper bound, not an observed charge count.
|
|
55
|
+
*
|
|
56
|
+
* `deliberate` is part of the type so future accounting cannot "tighten" the
|
|
57
|
+
* reservation to an optimistic count. Adaptive Anthropic may use three SDK
|
|
58
|
+
* sends under the provenance-locked adapter. A Codex WebSocket-capable request
|
|
59
|
+
* may send three socket frames and then one SSE request. Recovery reserves all
|
|
60
|
+
* sends that could be charged even though fewer (often one) normally occur.
|
|
61
|
+
*/
|
|
62
|
+
export type RecoverySendReservation =
|
|
63
|
+
| {
|
|
64
|
+
readonly maximumPossiblyChargedSends: 1;
|
|
65
|
+
readonly overReservation: "none";
|
|
66
|
+
readonly basis: "bounded-http";
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
readonly maximumPossiblyChargedSends: 3;
|
|
70
|
+
readonly overReservation: "deliberate";
|
|
71
|
+
readonly basis: "adaptive-anthropic-provenance";
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
readonly maximumPossiblyChargedSends: 4;
|
|
75
|
+
readonly overReservation: "deliberate";
|
|
76
|
+
readonly basis: "codex-websocket-uncertain";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export interface RecoverySendReservationRequest {
|
|
80
|
+
readonly ordinal: number;
|
|
81
|
+
readonly candidate: RecoveryCandidate;
|
|
82
|
+
readonly reservation: RecoverySendReservation;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Post-attempt charge exposure never converts transport uncertainty to zero.
|
|
87
|
+
* The bounded variant repeats the conservative pre-send upper bound; it is not
|
|
88
|
+
* a claim that every reserved send was charged.
|
|
89
|
+
*/
|
|
90
|
+
export type RecoveryChargedSendExposure =
|
|
91
|
+
| {
|
|
92
|
+
readonly status: "bounded";
|
|
93
|
+
readonly maximumPossiblyChargedSends: 1 | 3;
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
readonly status: "unknown";
|
|
97
|
+
readonly reason: "codex-websocket-send";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One supported provider invocation. `retrySafety` must classify the final
|
|
102
|
+
* outbound payload after caller payload replacement and every provider callback.
|
|
103
|
+
* Recovery independently overrides that classification for WebSocket-capable
|
|
104
|
+
* Codex attempts because a completed socket send has uncertain external effects.
|
|
105
|
+
*/
|
|
106
|
+
export interface RecoveryPhysicalAttempt {
|
|
107
|
+
readonly output: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>;
|
|
108
|
+
readonly retrySafety: RecoveryRetrySafety | Promise<RecoveryRetrySafety>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RecoveryDispatchRequest {
|
|
112
|
+
readonly candidate: RecoveryCandidate;
|
|
113
|
+
readonly context: unknown;
|
|
114
|
+
readonly options: RecoveryBoundStreamOptions;
|
|
115
|
+
readonly signal: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type RecoveryUsageCoverage =
|
|
119
|
+
| {
|
|
120
|
+
readonly coverage: "observed";
|
|
121
|
+
readonly inputTokens: number;
|
|
122
|
+
readonly outputTokens: number;
|
|
123
|
+
readonly cacheReadTokens: number;
|
|
124
|
+
readonly cacheWriteTokens: number;
|
|
125
|
+
readonly totalTokens: number;
|
|
126
|
+
}
|
|
127
|
+
| { readonly coverage: "gap" };
|
|
128
|
+
|
|
129
|
+
export type RecoveryCostCoverage =
|
|
130
|
+
| { readonly coverage: "observed"; readonly amount: number }
|
|
131
|
+
| { readonly coverage: "gap" };
|
|
132
|
+
|
|
133
|
+
export interface RecoveryAttemptAccounting {
|
|
134
|
+
readonly ordinal: number;
|
|
135
|
+
readonly candidate: RecoveryCandidate;
|
|
136
|
+
readonly disposition: "failed" | "accepted";
|
|
137
|
+
readonly failureCode?: AcceptedOutputErrorCode | "dispatch-failure";
|
|
138
|
+
readonly reservation: RecoverySendReservation;
|
|
139
|
+
readonly chargedSendExposure: RecoveryChargedSendExposure;
|
|
140
|
+
readonly usage: RecoveryUsageCoverage;
|
|
141
|
+
readonly cost: RecoveryCostCoverage;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface RecoveryEngineDependencies {
|
|
145
|
+
readonly clock: RecoveryClock;
|
|
146
|
+
/** Re-read eligibility, live config authorization and credentials here. */
|
|
147
|
+
readonly recheck: (
|
|
148
|
+
candidate: RecoveryCandidate,
|
|
149
|
+
signal: AbortSignal,
|
|
150
|
+
) => RecoveryPreDispatchDecision | Promise<RecoveryPreDispatchDecision>;
|
|
151
|
+
/** Reserve every possibly charged send before the provider can be invoked. */
|
|
152
|
+
readonly reserve: (
|
|
153
|
+
request: RecoverySendReservationRequest,
|
|
154
|
+
signal: AbortSignal,
|
|
155
|
+
) => "reserved" | "rejected" | Promise<"reserved" | "rejected">;
|
|
156
|
+
readonly dispatch: (
|
|
157
|
+
request: RecoveryDispatchRequest,
|
|
158
|
+
) => RecoveryPhysicalAttempt | Promise<RecoveryPhysicalAttempt>;
|
|
159
|
+
/** Every invoked physical attempt reaches this barrier exactly once. */
|
|
160
|
+
readonly account: (
|
|
161
|
+
attempt: RecoveryAttemptAccounting,
|
|
162
|
+
signal: AbortSignal,
|
|
163
|
+
) => "recorded" | "rejected" | Promise<"recorded" | "rejected">;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type RecoveryTimingConfig = Pick<
|
|
167
|
+
MultiAccountConfig,
|
|
168
|
+
"recoveryIdleTimeoutMs" | "recoveryAbsoluteTimeoutMs"
|
|
169
|
+
>;
|
|
170
|
+
|
|
171
|
+
export interface RecoveryRequest {
|
|
172
|
+
readonly candidates: readonly RecoveryCandidate[];
|
|
173
|
+
readonly context: unknown;
|
|
174
|
+
readonly options?: SimpleStreamOptions;
|
|
175
|
+
readonly timing: RecoveryTimingConfig;
|
|
176
|
+
readonly signal?: AbortSignal;
|
|
177
|
+
readonly deadlineMs?: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type RecoveryTerminationReason =
|
|
181
|
+
| "caller-aborted"
|
|
182
|
+
| "caller-deadline"
|
|
183
|
+
| "idle-timeout"
|
|
184
|
+
| "absolute-timeout"
|
|
185
|
+
| "reload"
|
|
186
|
+
| "shutdown"
|
|
187
|
+
| "malformed-config"
|
|
188
|
+
| "reservation-rejected"
|
|
189
|
+
| "accounting-rejected"
|
|
190
|
+
| "callback-failure"
|
|
191
|
+
| "attempt-aborted"
|
|
192
|
+
| "uncertain-external-effects"
|
|
193
|
+
| "completed-tool"
|
|
194
|
+
| "completed-call"
|
|
195
|
+
| "agent-run";
|
|
196
|
+
|
|
197
|
+
export type RecoveryResult =
|
|
198
|
+
| {
|
|
199
|
+
readonly status: "accepted";
|
|
200
|
+
readonly candidate: RecoveryCandidate;
|
|
201
|
+
readonly attempts: number;
|
|
202
|
+
readonly terminal: AssistantMessage;
|
|
203
|
+
readonly output: AssistantMessageEventStream;
|
|
204
|
+
}
|
|
205
|
+
| { readonly status: "exhausted"; readonly attempts: number }
|
|
206
|
+
| {
|
|
207
|
+
readonly status: "terminated";
|
|
208
|
+
readonly reason: RecoveryTerminationReason;
|
|
209
|
+
readonly attempts: number;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export interface RecoveryEngine {
|
|
213
|
+
recover(request: RecoveryRequest): Promise<RecoveryResult>;
|
|
214
|
+
/** Abort active requests; later requests use the newly published generation. */
|
|
215
|
+
reload(): void;
|
|
216
|
+
/** Permanently stop this engine and abort every active request. */
|
|
217
|
+
shutdown(): void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type AwaitResult<T> =
|
|
221
|
+
| { readonly status: "value"; readonly value: T }
|
|
222
|
+
| { readonly status: "rejected" }
|
|
223
|
+
| { readonly status: "aborted" };
|
|
224
|
+
|
|
225
|
+
interface AttemptFacts {
|
|
226
|
+
readonly usage: RecoveryUsageCoverage;
|
|
227
|
+
readonly cost: RecoveryCostCoverage;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface ActiveRequest {
|
|
231
|
+
readonly controller: AbortController;
|
|
232
|
+
reason?: RecoveryTerminationReason;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const GAP_USAGE: RecoveryUsageCoverage = Object.freeze({ coverage: "gap" });
|
|
236
|
+
const GAP_COST: RecoveryCostCoverage = Object.freeze({ coverage: "gap" });
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Supported request-local retry controls. Keep every managed family's entry
|
|
240
|
+
* separate: each provider path has its own compile-valid mutation control and
|
|
241
|
+
* regression test. The provenance-locked adaptive Anthropic adapter ignores
|
|
242
|
+
* this option, so its possible three SDK sends are over-reserved instead of
|
|
243
|
+
* assumed away. Google Antigravity has no supported inner-retry behavior yet,
|
|
244
|
+
* so it gets the same conservative zero the other non-Anthropic families use.
|
|
245
|
+
*/
|
|
246
|
+
const RECOVERY_INNER_RETRY_LIMITS = Object.freeze({
|
|
247
|
+
anthropic: 0,
|
|
248
|
+
openai: 0,
|
|
249
|
+
"openai-codex": 0,
|
|
250
|
+
"google-antigravity": 0,
|
|
251
|
+
}) satisfies Readonly<Record<RecoveryCandidate["family"], number>>;
|
|
252
|
+
|
|
253
|
+
function boundStreamOptions(
|
|
254
|
+
candidate: RecoveryCandidate,
|
|
255
|
+
options: SimpleStreamOptions | undefined,
|
|
256
|
+
): RecoveryBoundStreamOptions {
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
...(options ?? {}),
|
|
259
|
+
maxRetries: RECOVERY_INNER_RETRY_LIMITS[candidate.family],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sendReservationFor(
|
|
264
|
+
candidate: RecoveryCandidate,
|
|
265
|
+
options: RecoveryBoundStreamOptions,
|
|
266
|
+
): RecoverySendReservation {
|
|
267
|
+
if (candidate.family === "anthropic") {
|
|
268
|
+
// Recovery cannot see the model compat selector. Reserve the provenance-locked
|
|
269
|
+
// adaptive SDK's initial send plus two retries for every Anthropic candidate.
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
maximumPossiblyChargedSends: 3,
|
|
272
|
+
overReservation: "deliberate",
|
|
273
|
+
basis: "adaptive-anthropic-provenance",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (candidate.family === "openai-codex" && options.transport !== "sse") {
|
|
277
|
+
// Pinned Codex can reconnect twice after socket.send, then fall back to one
|
|
278
|
+
// bounded SSE send. The supported API exposes no narrower send observation.
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
maximumPossiblyChargedSends: 4,
|
|
281
|
+
overReservation: "deliberate",
|
|
282
|
+
basis: "codex-websocket-uncertain",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return Object.freeze({
|
|
286
|
+
maximumPossiblyChargedSends: 1,
|
|
287
|
+
overReservation: "none",
|
|
288
|
+
basis: "bounded-http",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function chargedSendExposureFor(
|
|
293
|
+
reservation: RecoverySendReservation,
|
|
294
|
+
): RecoveryChargedSendExposure {
|
|
295
|
+
return reservation.basis === "codex-websocket-uncertain"
|
|
296
|
+
? Object.freeze({ status: "unknown", reason: "codex-websocket-send" })
|
|
297
|
+
: Object.freeze({
|
|
298
|
+
status: "bounded",
|
|
299
|
+
maximumPossiblyChargedSends: reservation.maximumPossiblyChargedSends,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function finiteNonNegative(value: unknown): number | undefined {
|
|
304
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
305
|
+
? value
|
|
306
|
+
: undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function projectAttemptFacts(value: unknown): AttemptFacts | undefined {
|
|
310
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
311
|
+
const usage = (value as { usage?: unknown }).usage;
|
|
312
|
+
if (typeof usage !== "object" || usage === null) return undefined;
|
|
313
|
+
const record = usage as Record<string, unknown>;
|
|
314
|
+
const inputTokens = finiteNonNegative(record.input);
|
|
315
|
+
const outputTokens = finiteNonNegative(record.output);
|
|
316
|
+
const cacheReadTokens = finiteNonNegative(record.cacheRead);
|
|
317
|
+
const cacheWriteTokens = finiteNonNegative(record.cacheWrite);
|
|
318
|
+
const totalTokens = finiteNonNegative(record.totalTokens);
|
|
319
|
+
const cost =
|
|
320
|
+
typeof record.cost === "object" && record.cost !== null
|
|
321
|
+
? (record.cost as Record<string, unknown>)
|
|
322
|
+
: undefined;
|
|
323
|
+
const amount = finiteNonNegative(cost?.total);
|
|
324
|
+
return {
|
|
325
|
+
usage:
|
|
326
|
+
inputTokens === undefined ||
|
|
327
|
+
outputTokens === undefined ||
|
|
328
|
+
cacheReadTokens === undefined ||
|
|
329
|
+
cacheWriteTokens === undefined ||
|
|
330
|
+
totalTokens === undefined
|
|
331
|
+
? GAP_USAGE
|
|
332
|
+
: Object.freeze({
|
|
333
|
+
coverage: "observed",
|
|
334
|
+
inputTokens,
|
|
335
|
+
outputTokens,
|
|
336
|
+
cacheReadTokens,
|
|
337
|
+
cacheWriteTokens,
|
|
338
|
+
totalTokens,
|
|
339
|
+
}),
|
|
340
|
+
cost:
|
|
341
|
+
amount === undefined
|
|
342
|
+
? GAP_COST
|
|
343
|
+
: Object.freeze({ coverage: "observed", amount }),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function terminalFromEvent(event: unknown): unknown {
|
|
348
|
+
if (typeof event !== "object" || event === null) return undefined;
|
|
349
|
+
const record = event as Record<string, unknown>;
|
|
350
|
+
if (record.type === "done") return record.message;
|
|
351
|
+
if (record.type === "error") return record.error;
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const PROGRESS_EVENT_TYPES = new Set([
|
|
356
|
+
"start",
|
|
357
|
+
"text_start",
|
|
358
|
+
"text_delta",
|
|
359
|
+
"text_end",
|
|
360
|
+
"thinking_start",
|
|
361
|
+
"thinking_delta",
|
|
362
|
+
"thinking_end",
|
|
363
|
+
"toolcall_start",
|
|
364
|
+
"toolcall_delta",
|
|
365
|
+
"toolcall_end",
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
function isProgressEvent(event: unknown): boolean {
|
|
369
|
+
return (
|
|
370
|
+
typeof event === "object" &&
|
|
371
|
+
event !== null &&
|
|
372
|
+
PROGRESS_EVENT_TYPES.has((event as { type?: unknown }).type as string)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function observedOutput(
|
|
377
|
+
upstream: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>,
|
|
378
|
+
onProgress: () => void,
|
|
379
|
+
onFacts: (facts: AttemptFacts) => void,
|
|
380
|
+
): AsyncIterable<unknown> {
|
|
381
|
+
return {
|
|
382
|
+
async *[Symbol.asyncIterator]() {
|
|
383
|
+
const source = await upstream;
|
|
384
|
+
for await (const event of source) {
|
|
385
|
+
if (isProgressEvent(event)) onProgress();
|
|
386
|
+
const facts = projectAttemptFacts(terminalFromEvent(event));
|
|
387
|
+
if (facts !== undefined) onFacts(facts);
|
|
388
|
+
yield event;
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function validTiming(timing: RecoveryTimingConfig): boolean {
|
|
395
|
+
return (
|
|
396
|
+
Number.isFinite(timing.recoveryIdleTimeoutMs) &&
|
|
397
|
+
timing.recoveryIdleTimeoutMs >= 1_000 &&
|
|
398
|
+
Number.isFinite(timing.recoveryAbsoluteTimeoutMs) &&
|
|
399
|
+
timing.recoveryAbsoluteTimeoutMs >= 1_000
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function cancelTimer(timer: RecoveryTimer | undefined): void {
|
|
404
|
+
try {
|
|
405
|
+
timer?.cancel();
|
|
406
|
+
} catch {
|
|
407
|
+
// Cleanup is best-effort after the request already owns its terminal result.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function abortRequest(
|
|
412
|
+
request: ActiveRequest,
|
|
413
|
+
reason: RecoveryTerminationReason,
|
|
414
|
+
): void {
|
|
415
|
+
if (request.reason !== undefined) return;
|
|
416
|
+
request.reason = reason;
|
|
417
|
+
request.controller.abort();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function awaitRequest<T>(
|
|
421
|
+
value: T | Promise<T>,
|
|
422
|
+
request: ActiveRequest,
|
|
423
|
+
): Promise<AwaitResult<T>> {
|
|
424
|
+
if (request.controller.signal.aborted) return { status: "aborted" };
|
|
425
|
+
const operation = Promise.resolve(value);
|
|
426
|
+
void operation.catch(() => {});
|
|
427
|
+
let onAbort!: () => void;
|
|
428
|
+
const aborted = new Promise<AwaitResult<T>>((resolve) => {
|
|
429
|
+
onAbort = () => resolve({ status: "aborted" });
|
|
430
|
+
request.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
return await Promise.race([
|
|
434
|
+
operation.then(
|
|
435
|
+
(result): AwaitResult<T> => ({ status: "value", value: result }),
|
|
436
|
+
(): AwaitResult<T> => ({ status: "rejected" }),
|
|
437
|
+
),
|
|
438
|
+
aborted,
|
|
439
|
+
]);
|
|
440
|
+
} finally {
|
|
441
|
+
request.controller.signal.removeEventListener("abort", onAbort);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function validCandidate(candidate: RecoveryCandidate): boolean {
|
|
446
|
+
return (
|
|
447
|
+
typeof candidate.providerId === "string" &&
|
|
448
|
+
candidate.providerId.length > 0 &&
|
|
449
|
+
// The canonical managed-family set is the single source of truth here so a
|
|
450
|
+
// future family addition cannot silently fall out of recovery consideration
|
|
451
|
+
// the way the prior hand-listed three-family check did for Google Antigravity.
|
|
452
|
+
isManagedFamily(candidate.family) &&
|
|
453
|
+
typeof candidate.modelId === "string" &&
|
|
454
|
+
candidate.modelId.length > 0
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function observeLatePhysicalAttempt(
|
|
459
|
+
pending: RecoveryPhysicalAttempt | Promise<RecoveryPhysicalAttempt>,
|
|
460
|
+
signal: AbortSignal,
|
|
461
|
+
): void {
|
|
462
|
+
void Promise.resolve(pending).then(
|
|
463
|
+
(physical) => {
|
|
464
|
+
if (typeof physical !== "object" || physical === null) return;
|
|
465
|
+
try {
|
|
466
|
+
void Promise.resolve(physical.retrySafety).catch(() => {});
|
|
467
|
+
} catch {
|
|
468
|
+
// A throwing accessor is contained like any other callback failure.
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const output = createAcceptedOutputStream(physical.output, { signal });
|
|
472
|
+
void output.result().catch(() => {});
|
|
473
|
+
} catch {
|
|
474
|
+
// A throwing accessor is contained like any other callback failure.
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
() => {},
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function validPreDispatchDecision(value: unknown): value is RecoveryPreDispatchDecision {
|
|
482
|
+
if (typeof value !== "object" || value === null) return false;
|
|
483
|
+
const decision = value as Record<string, unknown>;
|
|
484
|
+
return (
|
|
485
|
+
decision.status === "eligible" ||
|
|
486
|
+
decision.status === "skip" ||
|
|
487
|
+
(decision.status === "terminate" &&
|
|
488
|
+
(decision.reason === "malformed-config" || decision.reason === "callback-failure"))
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function validRetrySafety(value: unknown): value is RecoveryRetrySafety {
|
|
493
|
+
if (typeof value !== "object" || value === null) return false;
|
|
494
|
+
const safety = value as Record<string, unknown>;
|
|
495
|
+
if (safety.status === "retryable") {
|
|
496
|
+
return (
|
|
497
|
+
safety.reason === "generation-only-incomplete" ||
|
|
498
|
+
safety.reason === "definitively-rejected"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return (
|
|
502
|
+
safety.status === "unsafe" &&
|
|
503
|
+
(safety.reason === "uncertain-external-effects" ||
|
|
504
|
+
safety.reason === "completed-tool" ||
|
|
505
|
+
safety.reason === "completed-call" ||
|
|
506
|
+
safety.reason === "agent-run")
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function reserveAttempt(
|
|
511
|
+
deps: RecoveryEngineDependencies,
|
|
512
|
+
request: ActiveRequest,
|
|
513
|
+
record: RecoverySendReservationRequest,
|
|
514
|
+
): Promise<"reserved" | "rejected" | "aborted"> {
|
|
515
|
+
let result: ReturnType<RecoveryEngineDependencies["reserve"]>;
|
|
516
|
+
try {
|
|
517
|
+
result = deps.reserve(record, request.controller.signal);
|
|
518
|
+
} catch {
|
|
519
|
+
return "rejected";
|
|
520
|
+
}
|
|
521
|
+
const pending = Promise.resolve(result);
|
|
522
|
+
void pending.catch(() => {});
|
|
523
|
+
if (request.controller.signal.aborted) return "aborted";
|
|
524
|
+
const settled = await awaitRequest(pending, request);
|
|
525
|
+
if (settled.status === "aborted") return "aborted";
|
|
526
|
+
if (settled.status === "rejected" || settled.value !== "reserved") {
|
|
527
|
+
return "rejected";
|
|
528
|
+
}
|
|
529
|
+
return "reserved";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function accountAttempt(
|
|
533
|
+
deps: RecoveryEngineDependencies,
|
|
534
|
+
request: ActiveRequest,
|
|
535
|
+
record: RecoveryAttemptAccounting,
|
|
536
|
+
): Promise<"recorded" | "rejected" | "aborted"> {
|
|
537
|
+
let result: ReturnType<RecoveryEngineDependencies["account"]>;
|
|
538
|
+
try {
|
|
539
|
+
result = deps.account(record, request.controller.signal);
|
|
540
|
+
} catch {
|
|
541
|
+
return "rejected";
|
|
542
|
+
}
|
|
543
|
+
const pending = Promise.resolve(result);
|
|
544
|
+
void pending.catch(() => {});
|
|
545
|
+
if (request.controller.signal.aborted) return "aborted";
|
|
546
|
+
const settled = await awaitRequest(pending, request);
|
|
547
|
+
if (settled.status === "aborted") return "aborted";
|
|
548
|
+
if (settled.status === "rejected" || settled.value !== "recorded") {
|
|
549
|
+
return "rejected";
|
|
550
|
+
}
|
|
551
|
+
return "recorded";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function runRecovery(
|
|
555
|
+
deps: RecoveryEngineDependencies,
|
|
556
|
+
input: RecoveryRequest,
|
|
557
|
+
request: ActiveRequest,
|
|
558
|
+
onProgress: () => void,
|
|
559
|
+
): Promise<RecoveryResult> {
|
|
560
|
+
let attempts = 0;
|
|
561
|
+
const terminated = (): RecoveryResult => ({
|
|
562
|
+
status: "terminated",
|
|
563
|
+
reason: request.reason ?? "callback-failure",
|
|
564
|
+
attempts,
|
|
565
|
+
});
|
|
566
|
+
const consideredPairs = new Set<string>();
|
|
567
|
+
|
|
568
|
+
for (const candidate of [...input.candidates]) {
|
|
569
|
+
if (request.controller.signal.aborted) return terminated();
|
|
570
|
+
if (!validCandidate(candidate)) continue;
|
|
571
|
+
const pair = `${candidate.providerId}\u0000${candidate.modelId}`;
|
|
572
|
+
if (consideredPairs.has(pair)) continue;
|
|
573
|
+
consideredPairs.add(pair);
|
|
574
|
+
|
|
575
|
+
let checkValue: ReturnType<RecoveryEngineDependencies["recheck"]>;
|
|
576
|
+
try {
|
|
577
|
+
checkValue = deps.recheck(candidate, request.controller.signal);
|
|
578
|
+
} catch {
|
|
579
|
+
abortRequest(request, "callback-failure");
|
|
580
|
+
return terminated();
|
|
581
|
+
}
|
|
582
|
+
const check = await awaitRequest(checkValue, request);
|
|
583
|
+
if (check.status === "aborted") return terminated();
|
|
584
|
+
let validCheck = false;
|
|
585
|
+
if (check.status === "value") {
|
|
586
|
+
try {
|
|
587
|
+
validCheck = validPreDispatchDecision(check.value);
|
|
588
|
+
} catch {
|
|
589
|
+
validCheck = false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (check.status === "rejected" || !validCheck) {
|
|
593
|
+
abortRequest(request, "callback-failure");
|
|
594
|
+
return terminated();
|
|
595
|
+
}
|
|
596
|
+
if (check.value.status === "skip") continue;
|
|
597
|
+
if (check.value.status === "terminate") {
|
|
598
|
+
abortRequest(request, check.value.reason);
|
|
599
|
+
return terminated();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let streamOptions: RecoveryBoundStreamOptions;
|
|
603
|
+
let reservation: RecoverySendReservation;
|
|
604
|
+
try {
|
|
605
|
+
streamOptions = boundStreamOptions(candidate, input.options);
|
|
606
|
+
reservation = sendReservationFor(candidate, streamOptions);
|
|
607
|
+
} catch {
|
|
608
|
+
abortRequest(request, "callback-failure");
|
|
609
|
+
return terminated();
|
|
610
|
+
}
|
|
611
|
+
const reserved = await reserveAttempt(deps, request, {
|
|
612
|
+
ordinal: attempts + 1,
|
|
613
|
+
candidate,
|
|
614
|
+
reservation,
|
|
615
|
+
});
|
|
616
|
+
if (reserved === "aborted") return terminated();
|
|
617
|
+
if (reserved === "rejected") {
|
|
618
|
+
abortRequest(request, "reservation-rejected");
|
|
619
|
+
return terminated();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let facts: AttemptFacts = { usage: GAP_USAGE, cost: GAP_COST };
|
|
623
|
+
let failureCode: RecoveryAttemptAccounting["failureCode"] = "dispatch-failure";
|
|
624
|
+
let accepted:
|
|
625
|
+
| { readonly terminal: AssistantMessage; readonly output: AssistantMessageEventStream }
|
|
626
|
+
| undefined;
|
|
627
|
+
let safety: RecoveryRetrySafety | undefined;
|
|
628
|
+
let callbackFailed = false;
|
|
629
|
+
const attemptController = new AbortController();
|
|
630
|
+
const abortAttempt = (): void => attemptController.abort();
|
|
631
|
+
request.controller.signal.addEventListener("abort", abortAttempt, { once: true });
|
|
632
|
+
if (request.controller.signal.aborted) abortAttempt();
|
|
633
|
+
|
|
634
|
+
let physicalValue: ReturnType<RecoveryEngineDependencies["dispatch"]> | undefined;
|
|
635
|
+
if (!request.controller.signal.aborted) {
|
|
636
|
+
try {
|
|
637
|
+
// A physical attempt begins exactly when this callback is invoked.
|
|
638
|
+
attempts += 1;
|
|
639
|
+
physicalValue = deps.dispatch({
|
|
640
|
+
candidate,
|
|
641
|
+
context: input.context,
|
|
642
|
+
options: streamOptions,
|
|
643
|
+
signal: attemptController.signal,
|
|
644
|
+
});
|
|
645
|
+
} catch {
|
|
646
|
+
callbackFailed = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (physicalValue !== undefined) {
|
|
651
|
+
const physicalResult = await awaitRequest(physicalValue, request);
|
|
652
|
+
if (physicalResult.status === "aborted") {
|
|
653
|
+
observeLatePhysicalAttempt(physicalValue, attemptController.signal);
|
|
654
|
+
} else if (physicalResult.status === "rejected") {
|
|
655
|
+
callbackFailed = true;
|
|
656
|
+
} else if (physicalResult.status === "value") {
|
|
657
|
+
try {
|
|
658
|
+
const physical = physicalResult.value;
|
|
659
|
+
if (typeof physical !== "object" || physical === null) {
|
|
660
|
+
throw new TypeError("malformed physical attempt");
|
|
661
|
+
}
|
|
662
|
+
const safetyPromise = Promise.resolve(physical.retrySafety);
|
|
663
|
+
void safetyPromise.catch(() => {});
|
|
664
|
+
const output = createAcceptedOutputStream(
|
|
665
|
+
observedOutput(
|
|
666
|
+
physical.output,
|
|
667
|
+
onProgress,
|
|
668
|
+
(observed) => {
|
|
669
|
+
facts = observed;
|
|
670
|
+
},
|
|
671
|
+
),
|
|
672
|
+
{ signal: request.controller.signal },
|
|
673
|
+
);
|
|
674
|
+
const outputResult = await awaitRequest(output.result(), request);
|
|
675
|
+
if (outputResult.status === "value") {
|
|
676
|
+
accepted = { terminal: outputResult.value, output };
|
|
677
|
+
facts = projectAttemptFacts(outputResult.value) ?? facts;
|
|
678
|
+
} else if (outputResult.status === "rejected") {
|
|
679
|
+
try {
|
|
680
|
+
await output.result();
|
|
681
|
+
} catch (error) {
|
|
682
|
+
failureCode =
|
|
683
|
+
error instanceof AcceptedOutputError
|
|
684
|
+
? error.code
|
|
685
|
+
: "dispatch-failure";
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (accepted === undefined) {
|
|
689
|
+
if (reservation.basis === "codex-websocket-uncertain") {
|
|
690
|
+
// No supported hook distinguishes an unsent connection failure from
|
|
691
|
+
// a failure after socket.send. The latter may already be executing,
|
|
692
|
+
// so recovery must never dispatch another candidate.
|
|
693
|
+
safety = {
|
|
694
|
+
status: "unsafe",
|
|
695
|
+
reason: "uncertain-external-effects",
|
|
696
|
+
};
|
|
697
|
+
} else {
|
|
698
|
+
const safetyResult = await awaitRequest(safetyPromise, request);
|
|
699
|
+
if (
|
|
700
|
+
safetyResult.status === "value" &&
|
|
701
|
+
validRetrySafety(safetyResult.value)
|
|
702
|
+
) {
|
|
703
|
+
safety = safetyResult.value;
|
|
704
|
+
} else if (
|
|
705
|
+
safetyResult.status === "rejected" ||
|
|
706
|
+
safetyResult.status === "value"
|
|
707
|
+
) {
|
|
708
|
+
callbackFailed = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
callbackFailed = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const accounting = await accountAttempt(deps, request, {
|
|
719
|
+
ordinal: attempts,
|
|
720
|
+
candidate,
|
|
721
|
+
disposition: accepted === undefined ? "failed" : "accepted",
|
|
722
|
+
...(accepted === undefined ? { failureCode } : {}),
|
|
723
|
+
reservation,
|
|
724
|
+
chargedSendExposure: chargedSendExposureFor(reservation),
|
|
725
|
+
usage: facts.usage,
|
|
726
|
+
cost: facts.cost,
|
|
727
|
+
});
|
|
728
|
+
request.controller.signal.removeEventListener("abort", abortAttempt);
|
|
729
|
+
if (!attemptController.signal.aborted) abortAttempt();
|
|
730
|
+
if (request.controller.signal.aborted) return terminated();
|
|
731
|
+
if (accounting === "rejected") {
|
|
732
|
+
abortRequest(request, "accounting-rejected");
|
|
733
|
+
return terminated();
|
|
734
|
+
}
|
|
735
|
+
if (accepted !== undefined) {
|
|
736
|
+
return {
|
|
737
|
+
status: "accepted",
|
|
738
|
+
candidate,
|
|
739
|
+
attempts,
|
|
740
|
+
terminal: accepted.terminal,
|
|
741
|
+
output: accepted.output,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (callbackFailed || safety === undefined) {
|
|
745
|
+
abortRequest(request, "callback-failure");
|
|
746
|
+
return terminated();
|
|
747
|
+
}
|
|
748
|
+
if (safety.status === "unsafe") {
|
|
749
|
+
abortRequest(request, safety.reason);
|
|
750
|
+
return terminated();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return { status: "exhausted", attempts };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function createRecoveryEngine(
|
|
757
|
+
deps: RecoveryEngineDependencies,
|
|
758
|
+
): RecoveryEngine {
|
|
759
|
+
const active = new Set<ActiveRequest>();
|
|
760
|
+
let shutdown = false;
|
|
761
|
+
|
|
762
|
+
const terminateAll = (reason: "reload" | "shutdown"): void => {
|
|
763
|
+
if (reason === "shutdown") shutdown = true;
|
|
764
|
+
for (const request of active) abortRequest(request, reason);
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
async recover(input): Promise<RecoveryResult> {
|
|
769
|
+
if (shutdown) {
|
|
770
|
+
return { status: "terminated", reason: "shutdown", attempts: 0 };
|
|
771
|
+
}
|
|
772
|
+
if (
|
|
773
|
+
!validTiming(input.timing) ||
|
|
774
|
+
(input.deadlineMs !== undefined &&
|
|
775
|
+
(!Number.isFinite(input.deadlineMs) || input.deadlineMs < 0))
|
|
776
|
+
) {
|
|
777
|
+
return { status: "terminated", reason: "malformed-config", attempts: 0 };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const activeRequest: ActiveRequest = { controller: new AbortController() };
|
|
781
|
+
active.add(activeRequest);
|
|
782
|
+
let deadlineTimer: RecoveryTimer | undefined;
|
|
783
|
+
let idleTimer: RecoveryTimer | undefined;
|
|
784
|
+
let absoluteTimer: RecoveryTimer | undefined;
|
|
785
|
+
let callerAbort: (() => void) | undefined;
|
|
786
|
+
let idleGeneration = 0;
|
|
787
|
+
const resetIdle = (): void => {
|
|
788
|
+
if (activeRequest.controller.signal.aborted) return;
|
|
789
|
+
const generation = ++idleGeneration;
|
|
790
|
+
cancelTimer(idleTimer);
|
|
791
|
+
try {
|
|
792
|
+
idleTimer = deps.clock.setTimer(input.timing.recoveryIdleTimeoutMs, () => {
|
|
793
|
+
if (generation === idleGeneration) {
|
|
794
|
+
abortRequest(activeRequest, "idle-timeout");
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
} catch {
|
|
798
|
+
abortRequest(activeRequest, "callback-failure");
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
try {
|
|
802
|
+
if (input.signal?.aborted) abortRequest(activeRequest, "caller-aborted");
|
|
803
|
+
else if (input.signal !== undefined) {
|
|
804
|
+
callerAbort = () => abortRequest(activeRequest, "caller-aborted");
|
|
805
|
+
input.signal.addEventListener("abort", callerAbort, { once: true });
|
|
806
|
+
}
|
|
807
|
+
if (input.deadlineMs !== undefined && !activeRequest.controller.signal.aborted) {
|
|
808
|
+
try {
|
|
809
|
+
const remaining = input.deadlineMs - deps.clock.now();
|
|
810
|
+
if (remaining <= 0) abortRequest(activeRequest, "caller-deadline");
|
|
811
|
+
else {
|
|
812
|
+
deadlineTimer = deps.clock.setTimer(remaining, () => {
|
|
813
|
+
abortRequest(activeRequest, "caller-deadline");
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
abortRequest(activeRequest, "callback-failure");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
resetIdle();
|
|
821
|
+
if (!activeRequest.controller.signal.aborted) {
|
|
822
|
+
try {
|
|
823
|
+
absoluteTimer = deps.clock.setTimer(
|
|
824
|
+
input.timing.recoveryAbsoluteTimeoutMs,
|
|
825
|
+
() => abortRequest(activeRequest, "absolute-timeout"),
|
|
826
|
+
);
|
|
827
|
+
} catch {
|
|
828
|
+
abortRequest(activeRequest, "callback-failure");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (activeRequest.controller.signal.aborted) {
|
|
832
|
+
return {
|
|
833
|
+
status: "terminated",
|
|
834
|
+
reason: activeRequest.reason ?? "callback-failure",
|
|
835
|
+
attempts: 0,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return await runRecovery(deps, input, activeRequest, resetIdle);
|
|
839
|
+
} finally {
|
|
840
|
+
idleGeneration += 1;
|
|
841
|
+
cancelTimer(deadlineTimer);
|
|
842
|
+
cancelTimer(idleTimer);
|
|
843
|
+
cancelTimer(absoluteTimer);
|
|
844
|
+
if (callerAbort !== undefined) {
|
|
845
|
+
input.signal?.removeEventListener("abort", callerAbort);
|
|
846
|
+
}
|
|
847
|
+
active.delete(activeRequest);
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
reload: () => terminateAll("reload"),
|
|
851
|
+
shutdown: () => terminateAll("shutdown"),
|
|
852
|
+
};
|
|
853
|
+
}
|