@superblocksteam/vite-plugin-file-sync 2.0.130-next.1 → 2.0.130-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/tool-message-utils.d.ts.map +1 -1
- package/dist/ai-service/agent/tool-message-utils.js +30 -0
- package/dist/ai-service/agent/tool-message-utils.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts +11 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +144 -29
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
- package/dist/ai-service/agent/utils.d.ts.map +1 -1
- package/dist/ai-service/agent/utils.js +6 -1
- package/dist/ai-service/agent/utils.js.map +1 -1
- package/dist/ai-service/app-skills/helpers.d.ts.map +1 -1
- package/dist/ai-service/app-skills/helpers.js +17 -18
- package/dist/ai-service/app-skills/helpers.js.map +1 -1
- package/dist/ai-service/chat/chat-push-metrics.d.ts +12 -0
- package/dist/ai-service/chat/chat-push-metrics.d.ts.map +1 -0
- package/dist/ai-service/chat/chat-push-metrics.js +68 -0
- package/dist/ai-service/chat/chat-push-metrics.js.map +1 -0
- package/dist/ai-service/chat/chat-session-store.d.ts +61 -5
- package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
- package/dist/ai-service/chat/chat-session-store.js +214 -8
- package/dist/ai-service/chat/chat-session-store.js.map +1 -1
- package/dist/ai-service/chat/transcript-budget.d.ts +108 -0
- package/dist/ai-service/chat/transcript-budget.d.ts.map +1 -0
- package/dist/ai-service/chat/transcript-budget.js +175 -0
- package/dist/ai-service/chat/transcript-budget.js.map +1 -0
- package/dist/ai-service/chat/transcript-metrics.d.ts +16 -0
- package/dist/ai-service/chat/transcript-metrics.d.ts.map +1 -0
- package/dist/ai-service/chat/transcript-metrics.js +128 -0
- package/dist/ai-service/chat/transcript-metrics.js.map +1 -0
- package/dist/ai-service/context-download.d.ts.map +1 -1
- package/dist/ai-service/context-download.js +48 -1
- package/dist/ai-service/context-download.js.map +1 -1
- package/dist/ai-service/context-upload.d.ts.map +1 -1
- package/dist/ai-service/context-upload.js +10 -0
- package/dist/ai-service/context-upload.js.map +1 -1
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +4 -0
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/llm/clark-stream-telemetry-sink.d.ts +4 -0
- package/dist/ai-service/llm/clark-stream-telemetry-sink.d.ts.map +1 -0
- package/dist/ai-service/llm/clark-stream-telemetry-sink.js +105 -0
- package/dist/ai-service/llm/clark-stream-telemetry-sink.js.map +1 -0
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +63 -39
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.js +10 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context.d.ts +14 -0
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +94 -3
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts +10 -1
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/event-types.js +7 -1
- package/dist/ai-service/llm/context-v2/storage/event-types.js.map +1 -1
- package/dist/ai-service/llm/stream/config.d.ts +30 -0
- package/dist/ai-service/llm/stream/config.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/config.js +34 -17
- package/dist/ai-service/llm/stream/config.js.map +1 -1
- package/dist/ai-service/llm/stream/errors.d.ts +10 -1
- package/dist/ai-service/llm/stream/errors.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/errors.js +3 -1
- package/dist/ai-service/llm/stream/errors.js.map +1 -1
- package/dist/ai-service/llm/stream/idle-monitor.d.ts +34 -4
- package/dist/ai-service/llm/stream/idle-monitor.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/idle-monitor.js +90 -6
- package/dist/ai-service/llm/stream/idle-monitor.js.map +1 -1
- package/dist/ai-service/llm/stream/index.d.ts +1 -1
- package/dist/ai-service/llm/stream/index.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/index.js.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.js +11 -0
- package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.d.ts +19 -0
- package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.js +387 -33
- package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.d.ts +18 -2
- package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.js +76 -3
- package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
- package/dist/ai-service/llm/stream/session.d.ts +6 -0
- package/dist/ai-service/llm/stream/session.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/session.js +28 -0
- package/dist/ai-service/llm/stream/session.js.map +1 -1
- package/dist/ai-service/llm/stream/types.d.ts +1 -0
- package/dist/ai-service/llm/stream/types.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/wait-state-tracker.d.ts +54 -0
- package/dist/ai-service/llm/stream/wait-state-tracker.d.ts.map +1 -0
- package/dist/ai-service/llm/stream/wait-state-tracker.js +105 -0
- package/dist/ai-service/llm/stream/wait-state-tracker.js.map +1 -0
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +13 -7
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +55 -43
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/flush-pending-chat-pushes.d.ts +11 -0
- package/dist/ai-service/state-machine/handlers/flush-pending-chat-pushes.d.ts.map +1 -0
- package/dist/ai-service/state-machine/handlers/flush-pending-chat-pushes.js +11 -0
- package/dist/ai-service/state-machine/handlers/flush-pending-chat-pushes.js.map +1 -0
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +10 -0
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/background-chat-record-metrics.d.ts +8 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record-metrics.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record-metrics.js +52 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record-metrics.js.map +1 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record.d.ts +33 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record.js +61 -0
- package/dist/ai-service/state-machine/helpers/background-chat-record.js.map +1 -0
- package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
- package/dist/ai-service/state-machine/mocks.js +1 -0
- package/dist/ai-service/state-machine/mocks.js.map +1 -1
- package/dist/ai-service/tasks/peer-push.d.ts +7 -4
- package/dist/ai-service/tasks/peer-push.d.ts.map +1 -1
- package/dist/ai-service/tasks/peer-push.js +7 -4
- package/dist/ai-service/tasks/peer-push.js.map +1 -1
- package/dist/migration-templates/app-fullstack/package.json +1 -1
- package/dist/socket-manager.d.ts +2 -1
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +17 -6
- package/dist/socket-manager.js.map +1 -1
- package/dist/util/logger.d.ts +1 -0
- package/dist/util/logger.d.ts.map +1 -1
- package/dist/util/logger.js.map +1 -1
- package/package.json +8 -8
|
@@ -1,11 +1,71 @@
|
|
|
1
1
|
import { getErrorMeta, } from "../../../util/logger.js";
|
|
2
2
|
import { buildThinkingProviderOptions } from "../../util/llm-config-utils.js";
|
|
3
3
|
import { LLMProviderError } from "../error.js";
|
|
4
|
+
import { getServerCompactionTextBlockUpdate } from "./compaction-text-block.js";
|
|
4
5
|
import { AbortedError, RetriesExhaustedError } from "./errors.js";
|
|
5
6
|
import { IdleMonitor } from "./idle-monitor.js";
|
|
6
7
|
import { createManagedStream } from "./managed-stream.js";
|
|
7
8
|
import { RetryEngine } from "./retry-engine.js";
|
|
8
9
|
import { StreamSession } from "./session.js";
|
|
10
|
+
import { WaitStateTracker } from "./wait-state-tracker.js";
|
|
11
|
+
/**
|
|
12
|
+
* Tracks the in-flight provider attempt for a single execute() call so a
|
|
13
|
+
* superseded attempt can be cut off the instant we decide to retry.
|
|
14
|
+
*
|
|
15
|
+
* Each attempt (the initial stream and every retry) gets its own AbortController
|
|
16
|
+
* chained to the execute-level parent signal. The Vercel AI SDK keeps draining a
|
|
17
|
+
* streamText() result and auto-executing tools regardless of whether we read its
|
|
18
|
+
* fullStream, so abandoning the stream is not enough — the only thing that stops
|
|
19
|
+
* a superseded attempt is aborting its signal. The orchestrator aborts the
|
|
20
|
+
* current attempt at the retry decision, before the backoff delay, so the loser
|
|
21
|
+
* can't race the winner to run terminal tools.
|
|
22
|
+
*/
|
|
23
|
+
class AttemptCoordinator {
|
|
24
|
+
parent;
|
|
25
|
+
current = null;
|
|
26
|
+
parentListener = null;
|
|
27
|
+
constructor(parent) {
|
|
28
|
+
this.parent = parent;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Begin a new attempt. Returns the attempt's abort signal and a predicate that
|
|
32
|
+
* is true only while this attempt is the live one and neither it nor the parent
|
|
33
|
+
* has been aborted. Used to fence tool execution and lifecycle callbacks so a
|
|
34
|
+
* superseded attempt's late, buffered events are ignored.
|
|
35
|
+
*/
|
|
36
|
+
begin() {
|
|
37
|
+
// Drop the superseded attempt's parent listener so they don't accumulate on
|
|
38
|
+
// the session signal for the whole execute() lifetime.
|
|
39
|
+
this.detachParentListener();
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
if (this.parent.aborted) {
|
|
42
|
+
controller.abort();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const onParentAbort = () => controller.abort();
|
|
46
|
+
this.parentListener = onParentAbort;
|
|
47
|
+
this.parent.addEventListener("abort", onParentAbort, { once: true });
|
|
48
|
+
}
|
|
49
|
+
this.current = controller;
|
|
50
|
+
const isCurrent = () => this.current === controller && // still the most-recent begin() call
|
|
51
|
+
!controller.signal.aborted && // not superseded via abortCurrent()
|
|
52
|
+
!this.parent.aborted; // defensive: parent abort also cascades via listener
|
|
53
|
+
return { signal: controller.signal, isCurrent };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Abort the in-flight attempt. Called at the retry decision, before backoff,
|
|
57
|
+
* to close the window where a stale attempt could wake up and execute tools.
|
|
58
|
+
*/
|
|
59
|
+
abortCurrent() {
|
|
60
|
+
this.current?.abort();
|
|
61
|
+
}
|
|
62
|
+
detachParentListener() {
|
|
63
|
+
if (this.parentListener) {
|
|
64
|
+
this.parent.removeEventListener("abort", this.parentListener);
|
|
65
|
+
this.parentListener = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
9
69
|
/**
|
|
10
70
|
* The main coordinator for stream operations.
|
|
11
71
|
*
|
|
@@ -65,7 +125,11 @@ export class StreamOrchestrator {
|
|
|
65
125
|
applicationId: this.applicationId,
|
|
66
126
|
getJwt: this.getJwt,
|
|
67
127
|
});
|
|
128
|
+
// Start watching before any data flows so that, if the stream stalls, we
|
|
129
|
+
// can say what it was stuck on (the model vs. a local tool or step gap).
|
|
130
|
+
const waitStateTracker = new WaitStateTracker(session);
|
|
68
131
|
const internalAbortController = new AbortController();
|
|
132
|
+
const attempts = new AttemptCoordinator(internalAbortController.signal);
|
|
69
133
|
if (options.abortSignal) {
|
|
70
134
|
options.abortSignal.addEventListener("abort", () => {
|
|
71
135
|
internalAbortController.abort();
|
|
@@ -88,28 +152,142 @@ export class StreamOrchestrator {
|
|
|
88
152
|
internalAbortController.signal.addEventListener("abort", () => {
|
|
89
153
|
void session.events.emit("session:abort", session);
|
|
90
154
|
});
|
|
91
|
-
// Idle monitoring: aborts hung streams that stop sending chunks
|
|
155
|
+
// Idle monitoring: aborts hung streams that stop sending chunks. Budgets
|
|
156
|
+
// are resolved per attempt from the CURRENT model (session.provider.model)
|
|
157
|
+
// so a fallback to a different model is monitored against its own budget,
|
|
158
|
+
// not the initial model's frozen one. See APPS-4775.
|
|
92
159
|
const idleMonitor = new IdleMonitor({
|
|
93
160
|
session,
|
|
94
|
-
maxIdleTimeMs: this.config.retry.idleTimeoutMs,
|
|
95
|
-
firstChunkTimeoutMs: this.config.retry.
|
|
161
|
+
maxIdleTimeMs: () => this.config.retry.budgetForModel(session.provider.model).idleTimeoutMs,
|
|
162
|
+
firstChunkTimeoutMs: () => this.config.retry.budgetForModel(session.provider.model)
|
|
163
|
+
.firstChunkTimeoutMs,
|
|
96
164
|
onServerCompactionStart: (blockId) => {
|
|
165
|
+
// firstChunkTimeoutMs is re-resolved here from the current model rather
|
|
166
|
+
// than read from the armed timer. The model only changes on fallback
|
|
167
|
+
// events (never mid-stream), so at compaction-start time it matches the
|
|
168
|
+
// budget the first-chunk timer was actually armed with.
|
|
97
169
|
this.logger.debug("[orchestrator] Server-side compaction chunk detected; holding first-chunk idle budget", {
|
|
98
170
|
sessionId: session.id,
|
|
99
171
|
provider: session.provider.name,
|
|
100
172
|
model: session.provider.model,
|
|
101
173
|
attempt: session.attempt,
|
|
102
174
|
blockId,
|
|
103
|
-
firstChunkTimeoutMs: this.config.retry.firstChunkTimeoutMs,
|
|
175
|
+
firstChunkTimeoutMs: this.config.retry.budgetForModel(session.provider.model).firstChunkTimeoutMs,
|
|
104
176
|
});
|
|
105
177
|
},
|
|
178
|
+
shouldPause: () => {
|
|
179
|
+
// Reads only plain in-memory state, so it should never throw. Guard it
|
|
180
|
+
// anyway and log if it does: a silent failure here would let a false
|
|
181
|
+
// stall through and rotate the provider — the exact bug this prevents —
|
|
182
|
+
// with no signal for oncall to find it. On error, return false so the
|
|
183
|
+
// stall still fires (failing safe toward surfacing a real stall).
|
|
184
|
+
try {
|
|
185
|
+
// Only a genuine wait on the provider's next byte should count toward
|
|
186
|
+
// a stall. If a tool is running or we're between steps, the gap is
|
|
187
|
+
// local work — not the model going silent — so pause instead of
|
|
188
|
+
// firing a false stall that would pointlessly rotate providers. See
|
|
189
|
+
// APPS-4605.
|
|
190
|
+
const waitState = waitStateTracker.getWaitState();
|
|
191
|
+
if (waitState.state === "tool" ||
|
|
192
|
+
waitState.state === "step-boundary") {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
// isAwaitingUpstream() is true only while the managed stream is
|
|
196
|
+
// blocked waiting for the provider's next chunk. False means the
|
|
197
|
+
// consumer took a chunk and hasn't asked for the next one yet (e.g.
|
|
198
|
+
// it's busy on its own downstream call), so the silence is on the
|
|
199
|
+
// consumer's side, not the provider's. Require at least one chunk to
|
|
200
|
+
// have arrived first: before that the gap is real provider silence
|
|
201
|
+
// (and the flag may not be set yet because the read happens lazily),
|
|
202
|
+
// so it must never be mistaken for the consumer holding the stream.
|
|
203
|
+
//
|
|
204
|
+
// Trade-off: if the provider connection drops while the consumer is
|
|
205
|
+
// holding a chunk, the dead connection is invisible until the
|
|
206
|
+
// consumer pulls again, since we suppress the stall for the whole
|
|
207
|
+
// consumer-hold window. That is acceptable because a slow consumer is
|
|
208
|
+
// the intended case and the consumer (the dev server request) carries
|
|
209
|
+
// its own request timeout that aborts the stream if it never resumes.
|
|
210
|
+
return (!session.isAwaitingUpstream() && session.metrics.chunkCount > 0);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
this.logger.error("[orchestrator] shouldPause check failed", {
|
|
214
|
+
error: {
|
|
215
|
+
kind: err instanceof Error ? err.name : "unknown",
|
|
216
|
+
message: err instanceof Error ? err.message : String(err),
|
|
217
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
onPause: () => {
|
|
224
|
+
// One structured event at the start of each local-wait stretch (a tool
|
|
225
|
+
// running, a step gap, or the consumer holding the stream past the idle
|
|
226
|
+
// budget). No stream_stall fires for these, so without this a hung tool
|
|
227
|
+
// would be invisible to the stream layer until the request aborts. The
|
|
228
|
+
// idle monitor fires this once per stretch, so a healthy slow tool logs
|
|
229
|
+
// once, not once per re-arm. See APPS-4605.
|
|
230
|
+
const waitState = waitStateTracker.getWaitState();
|
|
231
|
+
const message = `[orchestrator] Local wait suppressed idle stall`;
|
|
232
|
+
const attributes = {
|
|
233
|
+
event: "stream_local_wait",
|
|
234
|
+
session_id: session.id,
|
|
235
|
+
provider: session.provider.name,
|
|
236
|
+
model: session.provider.model,
|
|
237
|
+
attempt: session.attempt,
|
|
238
|
+
elapsed_ms: session.getElapsedMs(),
|
|
239
|
+
chunks_received: session.metrics.chunkCount,
|
|
240
|
+
// "tool"/"step-boundary" come from the wait tracker; "consumer" is the
|
|
241
|
+
// remaining case (consumer holding the stream), which the tracker
|
|
242
|
+
// reports as "provider" since it does not track consumer-pull state.
|
|
243
|
+
wait_state: waitState.state === "provider" ? "consumer" : waitState.state,
|
|
244
|
+
};
|
|
245
|
+
if (waitState.state === "tool") {
|
|
246
|
+
attributes.tool_name = waitState.toolName;
|
|
247
|
+
attributes.tool_call_id = waitState.toolCallId;
|
|
248
|
+
attributes.pending_tool_count = waitState.pendingToolCount;
|
|
249
|
+
}
|
|
250
|
+
if (session.organizationId) {
|
|
251
|
+
attributes.organization_id = session.organizationId;
|
|
252
|
+
}
|
|
253
|
+
if (session.applicationId) {
|
|
254
|
+
attributes.application_id = session.applicationId;
|
|
255
|
+
}
|
|
256
|
+
if (this.logger.warnStructured) {
|
|
257
|
+
this.logger.warnStructured(message, attributes);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.logger.warn(message, attributes);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
106
263
|
onStall: (error) => {
|
|
107
264
|
// budgetKind is the keepalive-vs-content discriminator: "first-chunk"
|
|
108
265
|
// means the model never sent content (only silence/keep-alives) before
|
|
109
266
|
// the stall; "inter-chunk" means content was flowing and then stopped.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
267
|
+
// It is carried on the error (set when the timer armed) because budgets
|
|
268
|
+
// are now per-model and can't be re-derived by numeric comparison.
|
|
269
|
+
const budgetKind = error.budgetKind;
|
|
270
|
+
const msSinceLastPull = session.getTimeSinceLastPull();
|
|
271
|
+
// Discriminate network-dark vs consumer-blocked (APPS-4729). The
|
|
272
|
+
// managed stream flips isAwaitingUpstream() around its
|
|
273
|
+
// `await iterator.next()`: true means the stall fired while we were
|
|
274
|
+
// blocked reading upstream (the wire went dark); false means the
|
|
275
|
+
// consumer is holding the stream without pulling (e.g. blocked on a
|
|
276
|
+
// downstream RPC). A boolean rather than an ms_since_last_pull
|
|
277
|
+
// comparison, which is unreliable near the idle-budget boundary. The
|
|
278
|
+
// flag read here is consistent because IdleMonitor runs this onStall
|
|
279
|
+
// callback synchronously, before the rejected waitForStall() propagates
|
|
280
|
+
// into the managed stream's catch and clears the flag (see idle-monitor).
|
|
281
|
+
//
|
|
282
|
+
// A first-chunk stall is upstream silence by definition: no chunk was
|
|
283
|
+
// delivered, so the consumer cannot be holding the stream. Force
|
|
284
|
+
// "network-dark" rather than trusting the flag — pull() is lazy, so a
|
|
285
|
+
// first-chunk timer can fire before the loop's first await ever set it.
|
|
286
|
+
// budget_kind still separates a first-chunk network-dark from an
|
|
287
|
+
// inter-chunk one.
|
|
288
|
+
const stallReason = budgetKind === "first-chunk" || session.isAwaitingUpstream()
|
|
289
|
+
? "network-dark"
|
|
290
|
+
: "consumer-blocked";
|
|
113
291
|
const message = `[orchestrator] Idle timeout exceeded, triggering retry`;
|
|
114
292
|
// Emit as structured attributes (OTel log attributes -> Datadog facets)
|
|
115
293
|
// so stalls are queryable by provider/model/attempt and chartable,
|
|
@@ -118,6 +296,14 @@ export class StreamOrchestrator {
|
|
|
118
296
|
// awaiting-user.ts). budget_ms + budget_kind already capture which idle
|
|
119
297
|
// budget applied and its value, so the static config constants are not
|
|
120
298
|
// re-emitted per stall. See APPS-4643.
|
|
299
|
+
// wait_state says what the stall was stuck on: "provider" = the model
|
|
300
|
+
// went silent, "tool" = a local tool was still running (a slow tool,
|
|
301
|
+
// not the model), "step-boundary" = it was between steps.
|
|
302
|
+
// raw_reset_count is how many keep-alive pings arrived during this
|
|
303
|
+
// stretch of silence; a healthy-but-quiet model still pings, so 0 means
|
|
304
|
+
// no pings got through. Pairs with ms_since_last_pull (how recently the
|
|
305
|
+
// reader pulled).
|
|
306
|
+
const waitState = waitStateTracker.getWaitState();
|
|
121
307
|
const attributes = {
|
|
122
308
|
event: "stream_stall",
|
|
123
309
|
session_id: session.id,
|
|
@@ -129,7 +315,21 @@ export class StreamOrchestrator {
|
|
|
129
315
|
budget_ms: error.maxIdleTimeMs,
|
|
130
316
|
elapsed_ms: session.getElapsedMs(),
|
|
131
317
|
chunks_received: session.metrics.chunkCount,
|
|
318
|
+
stall_reason: stallReason,
|
|
319
|
+
wait_state: waitState.state,
|
|
320
|
+
raw_reset_count: waitStateTracker.getRawResetCount(),
|
|
132
321
|
};
|
|
322
|
+
if (waitState.state === "tool") {
|
|
323
|
+
attributes.tool_name = waitState.toolName;
|
|
324
|
+
attributes.tool_call_id = waitState.toolCallId;
|
|
325
|
+
attributes.pending_tool_count = waitState.pendingToolCount;
|
|
326
|
+
}
|
|
327
|
+
// Supporting numeric signal: how long since the consumer last pulled.
|
|
328
|
+
// stall_reason is the primary discriminator; this aids triage. Absent
|
|
329
|
+
// when the consumer never made a productive pull.
|
|
330
|
+
if (msSinceLastPull !== undefined) {
|
|
331
|
+
attributes.ms_since_last_pull = msSinceLastPull;
|
|
332
|
+
}
|
|
133
333
|
// Customer-scoped facets let on-call triage stalls by org/app during an
|
|
134
334
|
// incident; only emitted when the session carries them.
|
|
135
335
|
if (session.organizationId) {
|
|
@@ -146,19 +346,67 @@ export class StreamOrchestrator {
|
|
|
146
346
|
}
|
|
147
347
|
},
|
|
148
348
|
});
|
|
149
|
-
const result = await this.startStreamWithRetry(optionsWithAbort, session, retryEngine);
|
|
349
|
+
const result = await this.startStreamWithRetry(optionsWithAbort, session, retryEngine, attempts, waitStateTracker);
|
|
150
350
|
// Start monitoring now that the stream is active
|
|
151
351
|
idleMonitor.start();
|
|
152
352
|
// wrapper order matters here. managed stream throws errors (error chunks), retry wrapper catches them.
|
|
153
353
|
// Pass the idle monitor so it can interrupt hung streams.
|
|
154
354
|
const managedStream = createManagedStream(result.fullStream, session, idleMonitor);
|
|
155
|
-
const retryableFullStream = this.wrapWithRetry(managedStream, (r) => createManagedStream(r.fullStream, session, idleMonitor), optionsWithAbort, session, retryEngine, internalAbortController);
|
|
355
|
+
const retryableFullStream = this.wrapWithRetry(managedStream, (r) => createManagedStream(r.fullStream, session, idleMonitor), optionsWithAbort, session, retryEngine, internalAbortController, attempts, waitStateTracker);
|
|
356
|
+
// `tee()` preserves the public contract that fullStream and textStream can
|
|
357
|
+
// be consumed independently; the slower branch may buffer response chunks.
|
|
358
|
+
const [fullStream, textSourceStream] = retryableFullStream.tee();
|
|
156
359
|
return {
|
|
157
360
|
...result,
|
|
158
|
-
fullStream:
|
|
159
|
-
textStream:
|
|
361
|
+
fullStream: this.asAsyncIterableStream(fullStream),
|
|
362
|
+
textStream: this.textStreamFromFullStream(this.asAsyncIterableStream(textSourceStream)),
|
|
160
363
|
};
|
|
161
364
|
}
|
|
365
|
+
asAsyncIterableStream(stream) {
|
|
366
|
+
const asyncIterableStream = stream;
|
|
367
|
+
if (asyncIterableStream[Symbol.asyncIterator]) {
|
|
368
|
+
return asyncIterableStream;
|
|
369
|
+
}
|
|
370
|
+
return Object.assign(stream, {
|
|
371
|
+
async *[Symbol.asyncIterator]() {
|
|
372
|
+
const reader = stream.getReader();
|
|
373
|
+
try {
|
|
374
|
+
while (true) {
|
|
375
|
+
const result = await reader.read();
|
|
376
|
+
if (result.done) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
yield result.value;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
reader.releaseLock();
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
textStreamFromFullStream(stream) {
|
|
389
|
+
const compactionTextBlockIds = new Set();
|
|
390
|
+
return this.asAsyncIterableStream(stream.pipeThrough(new TransformStream({
|
|
391
|
+
transform(chunk, controller) {
|
|
392
|
+
const compactionBlock = getServerCompactionTextBlockUpdate(chunk);
|
|
393
|
+
if (compactionBlock.kind === "start") {
|
|
394
|
+
compactionTextBlockIds.add(compactionBlock.id);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (compactionBlock.kind === "part" &&
|
|
398
|
+
compactionTextBlockIds.has(compactionBlock.id)) {
|
|
399
|
+
if (compactionBlock.done) {
|
|
400
|
+
compactionTextBlockIds.delete(compactionBlock.id);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (chunk.type === "text-delta") {
|
|
405
|
+
controller.enqueue(chunk.text);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
})));
|
|
409
|
+
}
|
|
162
410
|
/**
|
|
163
411
|
* Wraps a fullStream with retry logic. When an error occurs (either thrown or
|
|
164
412
|
* as an error chunk), makes a retry decision and either creates a new stream
|
|
@@ -168,7 +416,7 @@ export class StreamOrchestrator {
|
|
|
168
416
|
* retries will silently fail (the new stream will close immediately because
|
|
169
417
|
* session.isPending() returns false).
|
|
170
418
|
*/
|
|
171
|
-
wrapWithRetry(initialStream, getNewStream, options, session, retryEngine, abortController) {
|
|
419
|
+
wrapWithRetry(initialStream, getNewStream, options, session, retryEngine, abortController, attempts, waitStateTracker) {
|
|
172
420
|
const handleRetry = this.handleRetryDecision.bind(this);
|
|
173
421
|
let currentStream = initialStream;
|
|
174
422
|
async function* createRetryIterator() {
|
|
@@ -184,7 +432,7 @@ export class StreamOrchestrator {
|
|
|
184
432
|
return;
|
|
185
433
|
}
|
|
186
434
|
catch (error) {
|
|
187
|
-
const retryResult = await handleRetry(error, options, session, retryEngine);
|
|
435
|
+
const retryResult = await handleRetry(error, options, session, retryEngine, attempts, waitStateTracker);
|
|
188
436
|
if (!retryResult.shouldRetry) {
|
|
189
437
|
// abort the internal signal so any in-flight stream processing stops.
|
|
190
438
|
// without this, tools keep getting called in the background since the stream is not closed.
|
|
@@ -221,8 +469,8 @@ export class StreamOrchestrator {
|
|
|
221
469
|
* Handle an error by making a retry decision. Returns either a new result to continue
|
|
222
470
|
* with, or the final error to throw.
|
|
223
471
|
*/
|
|
224
|
-
async handleRetryDecision(error, options, session, retryEngine) {
|
|
225
|
-
const decision = retryEngine.getRetryDecision(error, session, options.abortSignal);
|
|
472
|
+
async handleRetryDecision(error, options, session, retryEngine, attempts, waitStateTracker) {
|
|
473
|
+
const decision = retryEngine.getRetryDecision(error, session, options.abortSignal, waitStateTracker.getWaitState());
|
|
226
474
|
if (!decision.shouldRetry) {
|
|
227
475
|
this.logger.warn(`[orchestrator] Not retrying: reason=${decision.reason}`, getErrorMeta(error));
|
|
228
476
|
session.finalize("error", error);
|
|
@@ -231,26 +479,26 @@ export class StreamOrchestrator {
|
|
|
231
479
|
error: this.wrapFinalError(error, decision.reason ?? "unknown", session),
|
|
232
480
|
};
|
|
233
481
|
}
|
|
482
|
+
// Supersede the failed attempt now, before the backoff delay, so its
|
|
483
|
+
// abandoned stream can't keep executing tools while we wait to retry.
|
|
484
|
+
attempts.abortCurrent();
|
|
234
485
|
await session.events.emit("retry", error, session.attempt, decision.delayMs, session);
|
|
235
486
|
if (decision.newProvider) {
|
|
236
|
-
|
|
237
|
-
session.provider = decision.newProvider;
|
|
238
|
-
this.logger.warn(`[orchestrator] Switching provider: ${oldProvider.name}/${oldProvider.model} -> ${decision.newProvider.name}/${decision.newProvider.model}`, getErrorMeta(error));
|
|
239
|
-
await session.events.emit("fallback", oldProvider, decision.newProvider, this.getFallbackReason(error), session);
|
|
487
|
+
await this.switchProvider(session, decision.newProvider, decision.fastFallback === true, error);
|
|
240
488
|
}
|
|
241
489
|
session.recordRetry(decision.delayMs);
|
|
242
|
-
this.logger.warn(`[orchestrator] Retrying in ${decision.delayMs}ms (attempt ${session.attempt})`, getErrorMeta(error));
|
|
490
|
+
this.logger.warn(`[orchestrator] Retrying in ${decision.delayMs}ms (attempt ${session.attempt}) reason=${decision.reason ?? "retry"}`, getErrorMeta(error));
|
|
243
491
|
await this.delay(decision.delayMs, options.abortSignal);
|
|
244
|
-
const newResult = await this.startStreamWithRetry(options, session, retryEngine);
|
|
492
|
+
const newResult = await this.startStreamWithRetry(options, session, retryEngine, attempts, waitStateTracker);
|
|
245
493
|
return { shouldRetry: true, newResult };
|
|
246
494
|
}
|
|
247
|
-
async startStreamWithRetry(options, session, retryEngine) {
|
|
495
|
+
async startStreamWithRetry(options, session, retryEngine, attempts, waitStateTracker) {
|
|
248
496
|
while (true) {
|
|
249
497
|
try {
|
|
250
|
-
return await this.startStream(options, session);
|
|
498
|
+
return await this.startStream(options, session, attempts);
|
|
251
499
|
}
|
|
252
500
|
catch (error) {
|
|
253
|
-
const decision = retryEngine.getRetryDecision(error, session, options.abortSignal);
|
|
501
|
+
const decision = retryEngine.getRetryDecision(error, session, options.abortSignal, waitStateTracker.getWaitState());
|
|
254
502
|
if (!decision.shouldRetry) {
|
|
255
503
|
this.logger.error(`[orchestrator] Startup error, not retrying: reason=${decision.reason}`, {
|
|
256
504
|
error: {
|
|
@@ -262,15 +510,14 @@ export class StreamOrchestrator {
|
|
|
262
510
|
await session.events.emit("session:error", error, session);
|
|
263
511
|
throw this.wrapFinalError(error, decision.reason ?? "unknown", session);
|
|
264
512
|
}
|
|
513
|
+
// Supersede the failed attempt before backoff.
|
|
514
|
+
attempts.abortCurrent();
|
|
265
515
|
await session.events.emit("retry", error, session.attempt, decision.delayMs, session);
|
|
266
516
|
if (decision.newProvider) {
|
|
267
|
-
|
|
268
|
-
session.provider = decision.newProvider;
|
|
269
|
-
this.logger.warn(`[orchestrator] Switching provider: ${oldProvider.name}/${oldProvider.model} -> ${decision.newProvider.name}/${decision.newProvider.model}`, getErrorMeta(error));
|
|
270
|
-
await session.events.emit("fallback", oldProvider, decision.newProvider, this.getFallbackReason(error), session);
|
|
517
|
+
await this.switchProvider(session, decision.newProvider, decision.fastFallback === true, error);
|
|
271
518
|
}
|
|
272
519
|
session.recordRetry(decision.delayMs);
|
|
273
|
-
this.logger.info(`[orchestrator] Startup retry in ${decision.delayMs}ms (attempt ${session.attempt})`);
|
|
520
|
+
this.logger.info(`[orchestrator] Startup retry in ${decision.delayMs}ms (attempt ${session.attempt}) reason=${decision.reason ?? "retry"}`);
|
|
274
521
|
await this.delay(decision.delayMs, options.abortSignal);
|
|
275
522
|
}
|
|
276
523
|
}
|
|
@@ -293,15 +540,27 @@ export class StreamOrchestrator {
|
|
|
293
540
|
cause: error,
|
|
294
541
|
});
|
|
295
542
|
}
|
|
296
|
-
async startStream(options, session) {
|
|
543
|
+
async startStream(options, session, attempts) {
|
|
297
544
|
if (options.abortSignal?.aborted) {
|
|
298
545
|
throw new AbortedError();
|
|
299
546
|
}
|
|
300
547
|
const providerOptions = this.applyProvider(options, session.provider, session);
|
|
301
548
|
this.logger.debug(`[orchestrator] Attempt ${session.attempt} with provider=${session.provider.name} model=${session.provider.model}`);
|
|
549
|
+
// Each attempt runs on its own abort signal so a superseded attempt can be
|
|
550
|
+
// aborted without killing the winning one. `isCurrent` fences tool execution
|
|
551
|
+
// and lifecycle callbacks: the Vercel AI SDK can still deliver buffered tool
|
|
552
|
+
// calls and callbacks from a superseded attempt after abort (abort is
|
|
553
|
+
// cooperative), so the live-attempt check is what prevents duplicate tool
|
|
554
|
+
// execution and duplicate finish events.
|
|
555
|
+
const { signal, isCurrent } = attempts.begin();
|
|
302
556
|
return this.provider.streamText({
|
|
303
557
|
...providerOptions,
|
|
558
|
+
abortSignal: signal,
|
|
559
|
+
tools: this.guardToolsForAttempt(providerOptions.tools, isCurrent),
|
|
304
560
|
prepareStep: async (step) => {
|
|
561
|
+
if (!isCurrent()) {
|
|
562
|
+
throw new AbortedError();
|
|
563
|
+
}
|
|
305
564
|
const preparedStep = (await options.prepareStep?.(step)) ?? step;
|
|
306
565
|
await session.events.emit("step:start", {
|
|
307
566
|
stepNumber: preparedStep.stepNumber,
|
|
@@ -310,17 +569,61 @@ export class StreamOrchestrator {
|
|
|
310
569
|
return preparedStep;
|
|
311
570
|
},
|
|
312
571
|
onStepFinish: async (step) => {
|
|
572
|
+
if (!isCurrent()) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
313
575
|
await session.events.emit("step:finish", step, session);
|
|
314
576
|
await options.onStepFinish?.(step);
|
|
315
577
|
},
|
|
316
578
|
onFinish: async (finalResult) => {
|
|
579
|
+
if (!isCurrent()) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
317
582
|
session.setTotalTokens(finalResult.totalUsage?.totalTokens ?? 0);
|
|
318
583
|
await session.events.emit("stream:finish", finalResult, session);
|
|
319
584
|
return options.onFinish?.(finalResult);
|
|
320
585
|
},
|
|
321
|
-
onChunk:
|
|
586
|
+
onChunk: (event) => {
|
|
587
|
+
if (!isCurrent()) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
return options.onChunk?.(event);
|
|
591
|
+
},
|
|
322
592
|
});
|
|
323
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Wraps each tool's execute so only the live attempt can run it. The Vercel AI
|
|
596
|
+
* SDK auto-executes tools as their calls arrive on the provider stream, and a
|
|
597
|
+
* superseded attempt's stream can still deliver buffered tool calls after we
|
|
598
|
+
* abort it. Throwing AbortedError here ensures a terminal tool's real side
|
|
599
|
+
* effect (e.g. build_finalize) runs once, for the winning attempt only.
|
|
600
|
+
*/
|
|
601
|
+
guardToolsForAttempt(tools, isCurrent) {
|
|
602
|
+
if (!tools) {
|
|
603
|
+
return tools;
|
|
604
|
+
}
|
|
605
|
+
const guarded = {};
|
|
606
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
607
|
+
if (typeof tool?.execute !== "function") {
|
|
608
|
+
guarded[name] = tool;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const execute = tool.execute.bind(tool);
|
|
612
|
+
guarded[name] = {
|
|
613
|
+
...tool,
|
|
614
|
+
execute: async (input, executeOptions) => {
|
|
615
|
+
if (!isCurrent()) {
|
|
616
|
+
throw new AbortedError();
|
|
617
|
+
}
|
|
618
|
+
return execute(input, executeOptions);
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
// Each entry preserves the original tool (`...tool`) and only wraps
|
|
623
|
+
// `execute`, so the runtime shape still matches TOOLS; signal the deliberate
|
|
624
|
+
// widening so a reviewer doesn't read this as a checked narrowing.
|
|
625
|
+
return guarded;
|
|
626
|
+
}
|
|
324
627
|
applyProvider(options, provider, session) {
|
|
325
628
|
const baseOptions = this.config.fallback.enabled
|
|
326
629
|
? {
|
|
@@ -344,9 +647,22 @@ export class StreamOrchestrator {
|
|
|
344
647
|
const thinkingRebuilt = this.rebuildThinkingOptions(baseOptions, provider);
|
|
345
648
|
return {
|
|
346
649
|
...thinkingRebuilt,
|
|
650
|
+
// `thinkingRebuilt.headers` is already authoritative for the
|
|
651
|
+
// interleaved-thinking beta header. Don't re-merge `options.headers`
|
|
652
|
+
// here or a disabled/adaptive rebuild would re-introduce the stale beta.
|
|
347
653
|
headers: {
|
|
348
|
-
...options.headers,
|
|
349
654
|
...thinkingRebuilt.headers,
|
|
655
|
+
// Stamp the per-attempt stream identity so the server proxy can join
|
|
656
|
+
// its egress frame counts to this stream's client-side stream_stall
|
|
657
|
+
// event. The value is the same session.id the stall event logs as
|
|
658
|
+
// `session_id`; the proxy logs it as `stream_id` (the server log
|
|
659
|
+
// pipeline redacts any field whose name contains "session", so the
|
|
660
|
+
// proxy cannot reuse the `session_id` name). That join is what lets a
|
|
661
|
+
// sampled stall be decided as upstream-silent (proxy wrote nothing) vs
|
|
662
|
+
// proxy->client drop (proxy wrote frames the client never saw). Rides
|
|
663
|
+
// the existing x-superblocks-* header path.
|
|
664
|
+
"x-superblocks-stream-id": session.id,
|
|
665
|
+
"x-superblocks-stream-attempt": String(session.attempt),
|
|
350
666
|
},
|
|
351
667
|
providerOptions: {
|
|
352
668
|
...thinkingRebuilt.providerOptions,
|
|
@@ -380,6 +696,11 @@ export class StreamOrchestrator {
|
|
|
380
696
|
const existingBedrockFields = existingBedrock.additionalModelRequestFields ?? {};
|
|
381
697
|
const { thinking: _a, effort: _b, ...anthropicRest } = existingAnthropic;
|
|
382
698
|
const { thinking: _c, output_config: _d, anthropic_beta: _e, budget_tokens: _f, ...bedrockFieldsRest } = existingBedrockFields;
|
|
699
|
+
// `buildThinkingProviderOptions` is the sole source of the
|
|
700
|
+
// interleaved-thinking `anthropic-beta` HTTP header. Strip any stale value
|
|
701
|
+
// before applying the fresh headers so a disabled or adaptive-effort
|
|
702
|
+
// rebuild (both emit no header) stops advertising the beta.
|
|
703
|
+
const { "anthropic-beta": _staleBeta, ...headersWithoutThinkingBeta } = options.headers ?? {};
|
|
383
704
|
const anthropic = fresh.anthropic
|
|
384
705
|
? { ...anthropicRest, ...fresh.anthropic }
|
|
385
706
|
: anthropicRest;
|
|
@@ -399,7 +720,7 @@ export class StreamOrchestrator {
|
|
|
399
720
|
: { ...existingBedrock, additionalModelRequestFields: {} };
|
|
400
721
|
return {
|
|
401
722
|
...options,
|
|
402
|
-
headers: { ...
|
|
723
|
+
headers: { ...headersWithoutThinkingBeta, ...fresh.headers },
|
|
403
724
|
providerOptions: {
|
|
404
725
|
...existing,
|
|
405
726
|
anthropic: anthropic,
|
|
@@ -415,6 +736,39 @@ export class StreamOrchestrator {
|
|
|
415
736
|
}
|
|
416
737
|
return "error";
|
|
417
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Switch the session to a fallback provider, emit the structured switch log,
|
|
741
|
+
* and fire the "fallback" event. `fastFallback` distinguishes the stall
|
|
742
|
+
* fast-fallback path (a stall left the provider early) from a normal
|
|
743
|
+
* provider-exhaustion fallback, surfaced as a queryable Datadog facet so the
|
|
744
|
+
* fast-fallback behavior is verifiable in production. Error context for the
|
|
745
|
+
* triggering error is carried by the retry log that follows this call.
|
|
746
|
+
*/
|
|
747
|
+
async switchProvider(session, newProvider, fastFallback, error) {
|
|
748
|
+
const oldProvider = session.provider;
|
|
749
|
+
session.provider = newProvider;
|
|
750
|
+
const message = `[orchestrator] Switching provider: ${oldProvider.name}/${oldProvider.model} -> ` +
|
|
751
|
+
`${newProvider.name}/${newProvider.model}` +
|
|
752
|
+
(fastFallback ? " (stall fast-fallback)" : "");
|
|
753
|
+
const attributes = {
|
|
754
|
+
event: "provider_fallback",
|
|
755
|
+
session_id: session.id,
|
|
756
|
+
from_provider: oldProvider.name,
|
|
757
|
+
from_model: oldProvider.model,
|
|
758
|
+
to_provider: newProvider.name,
|
|
759
|
+
to_model: newProvider.model,
|
|
760
|
+
attempt: session.attempt,
|
|
761
|
+
fast_fallback: fastFallback,
|
|
762
|
+
error_kind: error instanceof Error ? error.name : "unknown",
|
|
763
|
+
};
|
|
764
|
+
if (this.logger.warnStructured) {
|
|
765
|
+
this.logger.warnStructured(message, attributes);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
this.logger.warn(message, attributes);
|
|
769
|
+
}
|
|
770
|
+
await session.events.emit("fallback", oldProvider, newProvider, this.getFallbackReason(error), session);
|
|
771
|
+
}
|
|
418
772
|
delay(ms, abortSignal) {
|
|
419
773
|
return new Promise((resolve) => {
|
|
420
774
|
if (abortSignal?.aborted) {
|