@meistrari/agent-core 0.1.13 → 0.2.0-experimental.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/README.md +32 -0
- package/docs/credential-isolation-loopback-2026-09-14.md +30 -0
- package/package.json +1 -1
- package/src/agents/claude/claude-event-mapper.ts +29 -6
- package/src/agents/claude/claude-run.ts +70 -2
- package/src/agents/codex/codex-run.ts +12 -2
- package/src/protocol/agent-command.ts +2 -0
- package/src/protocol/agent-usage.ts +1 -0
- package/src/provenance.gen.ts +3 -3
- package/src/steps/contract.ts +4 -1
- package/src/steps/projector.ts +17 -1
- package/src/supervisor/agent-provider-factory.ts +60 -5
- package/src/supervisor/bootstrap-binder.ts +3 -2
- package/src/supervisor/index.ts +1 -1
- package/src/supervisor/persistence/supervisor-store.ts +22 -8
- package/src/supervisor/runtime-handler.ts +30 -2
- package/src/supervisor-protocol/bootstrap.ts +4 -1
- package/src/supervisor-protocol/command-body.ts +4 -1
- package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +118 -12
- package/src/testing/index.ts +1 -1
- package/src/worker-runtime-client/command-pump.ts +85 -10
- package/src/worker-runtime-client/connection-attempt.ts +95 -20
- package/src/worker-runtime-client/frame-processor.ts +242 -58
- package/src/worker-runtime-client/index.ts +8 -2
- package/src/worker-runtime-client/ports.ts +34 -2
- package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
|
@@ -2,137 +2,182 @@ import type { SupervisorRpcCancelEnvelope, SupervisorRpcRequestEnvelope, Supervi
|
|
|
2
2
|
import type { ConnectionAuthorityStore, RpcDispatcher, SessionSandboxRef, SupervisorConnection } from './ports'
|
|
3
3
|
import { isAppError } from '../errors'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Preparation is awaited in frame order; the returned external operation runs concurrently. */
|
|
6
|
+
export type PrepareSupervisorRpc = (request: SupervisorRpcRequestEnvelope, signal: AbortSignal) => Promise<() => Promise<unknown>>
|
|
7
|
+
interface RpcError { code: string, message: string, retryable: boolean }
|
|
8
|
+
export interface PreparedSupervisorRpcOptions {
|
|
9
|
+
prepare: PrepareSupervisorRpc
|
|
10
|
+
send: (response: SupervisorRpcResponseEnvelope) => Promise<void>
|
|
11
|
+
onFailure: (error: unknown) => void
|
|
12
|
+
maximumConcurrentRequests?: number
|
|
13
|
+
maximumDeadlineMs?: number
|
|
14
|
+
capacityError?: RpcError
|
|
15
|
+
formatError?: (error: unknown, aborted: boolean) => RpcError | null
|
|
16
|
+
}
|
|
17
|
+
interface LegacyDependencies {
|
|
18
|
+
ref: SessionSandboxRef
|
|
19
|
+
generation: number
|
|
20
|
+
runtimeConnectionAttemptId: string
|
|
21
|
+
authorityStore: ConnectionAuthorityStore
|
|
22
|
+
dispatcher: RpcDispatcher
|
|
23
|
+
connection: SupervisorConnection
|
|
24
|
+
}
|
|
25
|
+
interface ActiveRequest {
|
|
26
|
+
stop: AbortController
|
|
27
|
+
cancelled: boolean
|
|
28
|
+
done: ReturnType<typeof Promise.withResolvers<void>>
|
|
29
|
+
}
|
|
6
30
|
|
|
31
|
+
/** One lifecycle implementation for both legacy authority ports and ordered preparation ports. */
|
|
7
32
|
export class SupervisorRpcRequestManager {
|
|
8
|
-
private readonly active = new Map<string,
|
|
33
|
+
private readonly active = new Map<string, ActiveRequest>()
|
|
9
34
|
private stopped = false
|
|
35
|
+
private readonly options: PreparedSupervisorRpcOptions
|
|
36
|
+
private readonly legacyConnection?: SupervisorConnection
|
|
10
37
|
|
|
11
|
-
constructor(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
38
|
+
constructor(dependencies: PreparedSupervisorRpcOptions | LegacyDependencies) {
|
|
39
|
+
this.options = 'prepare' in dependencies ? dependencies : legacyOptions(dependencies)
|
|
40
|
+
this.legacyConnection = 'prepare' in dependencies ? undefined : dependencies.connection
|
|
41
|
+
for (const limit of [this.options.maximumConcurrentRequests ?? 32, this.options.maximumDeadlineMs ?? 30000]) {
|
|
42
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
43
|
+
throw new RangeError('RPC limits must be positive safe integers')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
19
46
|
|
|
47
|
+
/** Compatibility entrypoint; the frame processor need not await external I/O. */
|
|
20
48
|
handle(request: SupervisorRpcRequestEnvelope): void {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
void this.request(request, new AbortController().signal).catch((error) => {
|
|
50
|
+
if (this.legacyConnection && error instanceof Error && error.message === 'duplicate_active_rpc_request') {
|
|
51
|
+
this.legacyConnection.close(1008, 'Supervisor RPC request ID was reused.')
|
|
52
|
+
this.stop(error)
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.failed(error)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async request(request: SupervisorRpcRequestEnvelope, parent: AbortSignal): Promise<void> {
|
|
61
|
+
if (this.stopped || parent.aborted)
|
|
26
62
|
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
code: 'agent-core.rpc-capacity-exceeded',
|
|
32
|
-
message: 'Supervisor RPC capacity is unavailable.',
|
|
33
|
-
retryable: true,
|
|
34
|
-
})
|
|
63
|
+
if (this.active.has(request.requestId))
|
|
64
|
+
throw new Error('duplicate_active_rpc_request')
|
|
65
|
+
if (this.active.size >= (this.options.maximumConcurrentRequests ?? 32)) {
|
|
66
|
+
await this.sendFailure(request.requestId, this.options.capacityError ?? { code: 'rpc_capacity_exceeded', message: 'rpc_capacity_exceeded', retryable: true })
|
|
35
67
|
return
|
|
36
68
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
const entry: ActiveRequest = { stop: new AbortController(), cancelled: false, done: Promise.withResolvers<void>() }
|
|
70
|
+
this.active.set(request.requestId, entry)
|
|
71
|
+
const deadline = new AbortController()
|
|
72
|
+
const timeout = setTimeout(() => deadline.abort(new DOMException('Supervisor RPC deadline elapsed.', 'TimeoutError')), Math.min(request.deadlineMs, this.options.maximumDeadlineMs ?? 30000))
|
|
41
73
|
timeout.unref?.()
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
const signal = AbortSignal.any([parent, entry.stop.signal, deadline.signal])
|
|
75
|
+
const finish = () => {
|
|
44
76
|
clearTimeout(timeout)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
77
|
+
this.active.delete(request.requestId)
|
|
78
|
+
entry.done.resolve()
|
|
79
|
+
}
|
|
80
|
+
const respondFailure = async (error: unknown) => {
|
|
81
|
+
if (this.stopped || parent.aborted || entry.cancelled)
|
|
82
|
+
return
|
|
83
|
+
const code = signal.aborted ? 'rpc_deadline_exceeded' : 'rpc_failed'
|
|
84
|
+
const failure = this.options.formatError
|
|
85
|
+
? this.options.formatError(error, signal.aborted)
|
|
86
|
+
: { code, message: code, retryable: signal.aborted }
|
|
87
|
+
if (failure)
|
|
88
|
+
await this.sendFailure(request.requestId, failure)
|
|
89
|
+
}
|
|
90
|
+
let execute: () => Promise<unknown>
|
|
91
|
+
try {
|
|
92
|
+
execute = await this.options.prepare(request, signal)
|
|
93
|
+
signal.throwIfAborted()
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
try {
|
|
97
|
+
await respondFailure(error)
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
finish()
|
|
101
|
+
}
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
void (async () => {
|
|
105
|
+
let value: unknown
|
|
106
|
+
try {
|
|
107
|
+
value = await execute()
|
|
108
|
+
signal.throwIfAborted()
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
await respondFailure(error)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!this.stopped && !entry.cancelled && !parent.aborted)
|
|
115
|
+
await this.options.send({ kind: 'rpc.response', requestId: request.requestId, result: { status: 'ok', value } })
|
|
116
|
+
})().catch(error => this.failed(error)).finally(finish)
|
|
48
117
|
}
|
|
49
118
|
|
|
50
|
-
cancel(frame: SupervisorRpcCancelEnvelope): void {
|
|
51
|
-
this.active.get(frame
|
|
119
|
+
cancel(frame: SupervisorRpcCancelEnvelope | string): void {
|
|
120
|
+
const entry = this.active.get(typeof frame === 'string' ? frame : frame.requestId)
|
|
121
|
+
if (entry) {
|
|
122
|
+
entry.cancelled = true
|
|
123
|
+
entry.stop.abort(new DOMException('Supervisor RPC was cancelled.', 'AbortError'))
|
|
124
|
+
}
|
|
52
125
|
}
|
|
53
126
|
|
|
54
127
|
stop(reason: unknown = new DOMException('Supervisor connection closed.', 'AbortError')): void {
|
|
55
|
-
if (this.stopped)
|
|
56
|
-
return
|
|
57
128
|
this.stopped = true
|
|
58
|
-
for (const
|
|
59
|
-
|
|
60
|
-
|
|
129
|
+
for (const entry of this.active.values()) {
|
|
130
|
+
entry.cancelled = true
|
|
131
|
+
entry.stop.abort(reason)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async drain(): Promise<void> {
|
|
136
|
+
await Promise.all([...this.active.values()].map(entry => entry.done.promise))
|
|
61
137
|
}
|
|
62
138
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
139
|
+
async close(): Promise<void> {
|
|
140
|
+
this.stop()
|
|
141
|
+
await this.drain()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async sendFailure(requestId: string, error: RpcError): Promise<void> {
|
|
145
|
+
if (!this.stopped)
|
|
146
|
+
await this.options.send({ kind: 'rpc.response', requestId, result: { status: 'error', error } })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private failed(error: unknown): void {
|
|
67
150
|
try {
|
|
68
|
-
|
|
69
|
-
sessionSandboxId: this.dependencies.ref.sessionSandboxId,
|
|
70
|
-
generation: this.dependencies.generation,
|
|
71
|
-
runtimeConnectionAttemptId: this.dependencies.runtimeConnectionAttemptId,
|
|
72
|
-
})
|
|
73
|
-
if (!authorized)
|
|
74
|
-
throw new RpcAuthorityLostError()
|
|
75
|
-
const value = await this.dependencies.dispatcher.handle({
|
|
76
|
-
...this.dependencies.ref,
|
|
77
|
-
request: input.request,
|
|
78
|
-
signal: input.controller.signal,
|
|
79
|
-
})
|
|
80
|
-
if (input.controller.signal.aborted || this.stopped)
|
|
81
|
-
return
|
|
82
|
-
await this.dependencies.connection.send(JSON.stringify({
|
|
83
|
-
kind: 'rpc.response',
|
|
84
|
-
requestId: input.request.requestId,
|
|
85
|
-
result: { status: 'ok', value },
|
|
86
|
-
} satisfies SupervisorRpcResponseEnvelope))
|
|
151
|
+
this.options.onFailure(error)
|
|
87
152
|
}
|
|
88
|
-
catch
|
|
89
|
-
|
|
90
|
-
return
|
|
91
|
-
if (error instanceof RpcAuthorityLostError) {
|
|
92
|
-
this.sendFailure({
|
|
93
|
-
requestId: input.request.requestId,
|
|
94
|
-
code: 'agent-core.connection-authority-lost',
|
|
95
|
-
message: 'Supervisor connection authority is no longer current.',
|
|
96
|
-
retryable: true,
|
|
97
|
-
})
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
if (isAppError(error)) {
|
|
101
|
-
this.sendFailure({
|
|
102
|
-
requestId: input.request.requestId,
|
|
103
|
-
code: error.code,
|
|
104
|
-
message: error.publicMessage,
|
|
105
|
-
retryable: error.retryable,
|
|
106
|
-
})
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
this.sendFailure({
|
|
110
|
-
requestId: input.request.requestId,
|
|
111
|
-
code: 'agent-core.rpc-failed',
|
|
112
|
-
message: 'Supervisor RPC failed.',
|
|
113
|
-
retryable: true,
|
|
114
|
-
})
|
|
153
|
+
catch {
|
|
154
|
+
this.stop(error)
|
|
115
155
|
}
|
|
116
156
|
}
|
|
157
|
+
}
|
|
117
158
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
function legacyOptions(dependencies: LegacyDependencies): PreparedSupervisorRpcOptions {
|
|
160
|
+
return {
|
|
161
|
+
maximumConcurrentRequests: 32,
|
|
162
|
+
maximumDeadlineMs: Number.MAX_SAFE_INTEGER,
|
|
163
|
+
prepare: async (request, signal) => {
|
|
164
|
+
const authorized = await dependencies.authorityStore.confirmConnectionAuthority({ sessionSandboxId: dependencies.ref.sessionSandboxId, generation: dependencies.generation, runtimeConnectionAttemptId: dependencies.runtimeConnectionAttemptId })
|
|
165
|
+
if (!authorized)
|
|
166
|
+
throw new RpcAuthorityLostError()
|
|
167
|
+
return () => dependencies.dispatcher.handle({ ...dependencies.ref, request, signal })
|
|
168
|
+
},
|
|
169
|
+
send: async (response) => { await dependencies.connection.send(JSON.stringify(response)) },
|
|
170
|
+
onFailure: () => { dependencies.connection.close(1011, 'Supervisor RPC response delivery failed.') },
|
|
171
|
+
capacityError: { code: 'agent-core.rpc-capacity-exceeded', message: 'Supervisor RPC capacity is unavailable.', retryable: true },
|
|
172
|
+
formatError: (error, aborted) => {
|
|
173
|
+
if (aborted)
|
|
174
|
+
return null
|
|
175
|
+
if (error instanceof RpcAuthorityLostError)
|
|
176
|
+
return { code: 'agent-core.connection-authority-lost', message: 'Supervisor connection authority is no longer current.', retryable: true }
|
|
177
|
+
if (isAppError(error))
|
|
178
|
+
return { code: error.code, message: error.publicMessage, retryable: error.retryable }
|
|
179
|
+
return { code: 'agent-core.rpc-failed', message: 'Supervisor RPC failed.', retryable: true }
|
|
180
|
+
},
|
|
136
181
|
}
|
|
137
182
|
}
|
|
138
183
|
|
|
@@ -151,34 +151,41 @@ class OwnedConnection {
|
|
|
151
151
|
this.claimed?.(claim.sessionId)
|
|
152
152
|
let establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
|
|
153
153
|
let backoff = this.dependencies.timings.retryInitialDelayMs
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
154
|
+
const exhaustedBudget = await runConnectionAttempts<boolean>({
|
|
155
|
+
signal: this.controller.signal,
|
|
156
|
+
attempt: async () => {
|
|
157
|
+
const attempt = new SessionSandboxConnectionAttempt({
|
|
158
|
+
claim,
|
|
159
|
+
establishmentDeadline,
|
|
160
|
+
replicaId: this.dependencies.replicaId,
|
|
161
|
+
trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
|
|
162
|
+
signControlAuthority: this.dependencies.signControlAuthority,
|
|
163
|
+
openSupervisorConnection: this.dependencies.openSupervisorConnection,
|
|
164
|
+
authorityStore: this.dependencies.authorityStore,
|
|
165
|
+
commands: this.dependencies.commands,
|
|
166
|
+
durableEvents: this.dependencies.durableEvents,
|
|
167
|
+
ephemeralEvents: this.dependencies.ephemeralEvents,
|
|
168
|
+
rpc: this.dependencies.rpc,
|
|
169
|
+
bootstrap: this.dependencies.bootstrap,
|
|
170
|
+
failures: this.dependencies.failures,
|
|
171
|
+
timings: this.dependencies.timings,
|
|
172
|
+
logger: this.dependencies.logger,
|
|
173
|
+
parentSignal: this.controller.signal,
|
|
174
|
+
})
|
|
175
|
+
this.attempt = attempt
|
|
176
|
+
try {
|
|
177
|
+
await attempt.run()
|
|
178
|
+
return { delayMs: 0 }
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
attempt.stop(error)
|
|
182
|
+
await attempt.drain()
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
onFailure: async (error) => {
|
|
180
187
|
if (this.controller.signal.aborted)
|
|
181
|
-
return
|
|
188
|
+
return { done: false }
|
|
182
189
|
if (error instanceof DeterministicEstablishmentError) {
|
|
183
190
|
if (!error.reported) {
|
|
184
191
|
await this.dependencies.failures.failDeterministicEstablishment({
|
|
@@ -190,10 +197,10 @@ class OwnedConnection {
|
|
|
190
197
|
detail: error.detail,
|
|
191
198
|
})
|
|
192
199
|
}
|
|
193
|
-
return
|
|
200
|
+
return { done: false }
|
|
194
201
|
}
|
|
195
202
|
if (error instanceof ConnectionAuthorityLostError)
|
|
196
|
-
return
|
|
203
|
+
return { done: false }
|
|
197
204
|
if (error instanceof TokenDecryptionError) {
|
|
198
205
|
await this.dependencies.failures.markSessionFailed({
|
|
199
206
|
sessionId: claim.sessionId,
|
|
@@ -201,7 +208,7 @@ class OwnedConnection {
|
|
|
201
208
|
providerSandboxId: claim.providerSandboxId,
|
|
202
209
|
code: 'agent-core.traffic-token-decryption-failed',
|
|
203
210
|
})
|
|
204
|
-
return
|
|
211
|
+
return { done: false }
|
|
205
212
|
}
|
|
206
213
|
if (error instanceof ConnectionClosedError && error.ready) {
|
|
207
214
|
establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
|
|
@@ -212,12 +219,13 @@ class OwnedConnection {
|
|
|
212
219
|
generation: claim.runtimeConnectionGeneration,
|
|
213
220
|
})
|
|
214
221
|
if (performance.now() + backoff >= establishmentDeadline)
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
return { done: true }
|
|
223
|
+
const delayMs = backoff
|
|
217
224
|
backoff = Math.min(backoff * 2, this.dependencies.timings.retryMaxDelayMs)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
return { delayMs }
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
if (exhaustedBudget && !this.controller.signal.aborted) {
|
|
221
229
|
const exhausted = await this.dependencies.authorityStore.markConnectionRetryExhausted({
|
|
222
230
|
sessionSandboxId: claim.sessionSandboxId,
|
|
223
231
|
generation: claim.runtimeConnectionGeneration,
|
|
@@ -299,3 +307,32 @@ function validatedTimings(timings: SandboxConnectionRuntimeTimings): SandboxConn
|
|
|
299
307
|
throw new RangeError('retryInitialDelayMs must not exceed retryMaxDelayMs.')
|
|
300
308
|
return timings
|
|
301
309
|
}
|
|
310
|
+
|
|
311
|
+
export type ConnectionAttemptOutcome<T> = { done: T } | { delayMs: number }
|
|
312
|
+
|
|
313
|
+
/** Retry scheduling only. Authority, failure classification and budgets belong to callers. */
|
|
314
|
+
export async function runConnectionAttempts<T>(options: {
|
|
315
|
+
signal: AbortSignal
|
|
316
|
+
attempt: (attemptNumber: number) => Promise<ConnectionAttemptOutcome<T>>
|
|
317
|
+
onFailure: (error: unknown, attemptNumber: number) => Promise<ConnectionAttemptOutcome<T>>
|
|
318
|
+
}): Promise<T> {
|
|
319
|
+
let attemptNumber = 0
|
|
320
|
+
while (true) {
|
|
321
|
+
options.signal.throwIfAborted()
|
|
322
|
+
let outcome: ConnectionAttemptOutcome<T>
|
|
323
|
+
try {
|
|
324
|
+
outcome = await options.attempt(attemptNumber)
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
options.signal.throwIfAborted()
|
|
328
|
+
outcome = await options.onFailure(error, attemptNumber)
|
|
329
|
+
}
|
|
330
|
+
options.signal.throwIfAborted()
|
|
331
|
+
if ('done' in outcome)
|
|
332
|
+
return outcome.done
|
|
333
|
+
if (!Number.isSafeInteger(outcome.delayMs) || outcome.delayMs < 0)
|
|
334
|
+
throw new RangeError('Connection retry delay must be a nonnegative safe integer.')
|
|
335
|
+
await abortableDelay(outcome.delayMs, options.signal)
|
|
336
|
+
attemptNumber++
|
|
337
|
+
}
|
|
338
|
+
}
|