@opengeni/core 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,11 +2,10 @@
2
2
  // real DB pointer + the live NATS control plane for the API-DIRECT Channel-A path
3
3
  // (M7). Symmetric with apps/worker/src/sandbox-routing.ts (the turn path).
4
4
  //
5
- // A Channel-A op resumes the group box by id and runs ONE op against it. With
6
- // hot-swap, the op must land on the session's CURRENTLY-active sandbox, not
7
- // always the group box: if the session swapped to a selfhosted machine, an
8
- // fs.read / git.status / exec from the API must reach THAT machine. So the
9
- // established group session is wrapped in a `RoutingSandboxSession` that re-reads
5
+ // A Channel-A op runs against one established home: either a lease-owned provider
6
+ // box or a Connected Machine home that intentionally has no cloud lease. With
7
+ // hot-swap, the op must land on the session's CURRENTLY-active sandbox. The
8
+ // established session is wrapped in a `RoutingSandboxSession` that re-reads
10
9
  // (active_sandbox_id, active_epoch) and dispatches to the active backend.
11
10
  //
12
11
  // The DB-coupled glue (readActiveSandbox / getSandbox / the selfhosted ControlRpc
@@ -16,10 +15,12 @@ import type { Settings } from "@opengeni/config";
16
15
  import {
17
16
  advanceWorkspaceGenerationForDirectRequest,
18
17
  advanceWorkspaceGenerationForRetainedProcess,
18
+ getRetainedProcess,
19
19
  getSandbox,
20
20
  markWarmLeaseInstanceLost,
21
21
  readActiveSandbox,
22
22
  retainWorkspaceMutationProcess,
23
+ retainedProcessSettlementIdentity,
23
24
  settleRetainedProcess,
24
25
  verifyDirectWorkspaceMutationSettlement,
25
26
  verifyRetainedProcessMutationSettlement,
@@ -55,7 +56,12 @@ export type ChannelARoutingServices = {
55
56
  export function relayConfigFromSettings(settings: Settings): SelfhostedRelayConfig {
56
57
  const raw = settings.selfhostedRelayUrl?.trim();
57
58
  if (!raw) {
58
- return { host: "relay.opengeni.local", port: 443, tls: true, path: "/stream" };
59
+ return {
60
+ host: "relay.opengeni.local",
61
+ port: 443,
62
+ tls: true,
63
+ path: "/stream",
64
+ };
59
65
  }
60
66
  try {
61
67
  const url = new URL(raw.includes("://") ? raw : `wss://${raw}`);
@@ -105,11 +111,10 @@ export function routingEnabled(settings: Settings): boolean {
105
111
  }
106
112
 
107
113
  /**
108
- * Wrap an established group-box session in a `RoutingSandboxSession` so a
109
- * Channel-A op routes to the session's currently-active sandbox. Returns the
110
- * established handle with its `session` replaced by the stable proxy. With the
111
- * default pointer (active_sandbox_id == null) this routes to the group box
112
- * unchanged; a selfhosted active pointer routes the op to the machine.
114
+ * Wrap an established home session in a `RoutingSandboxSession` so a Channel-A
115
+ * op routes to the session's currently-active sandbox. Provider homes supply a
116
+ * lease; machine homes supply a pinned selfhosted identity and no lease. Returns
117
+ * the established handle with its `session` replaced by the stable proxy.
113
118
  */
114
119
  export function wrapChannelABoxWithRouting(
115
120
  services: ChannelARoutingServices,
@@ -117,12 +122,19 @@ export function wrapChannelABoxWithRouting(
117
122
  accountId: string;
118
123
  workspaceId: string;
119
124
  sessionId: string;
120
- homeLease: {
125
+ homeLease?: {
121
126
  sandboxGroupId: string;
122
127
  leaseEpoch: number;
123
128
  instanceId: string;
124
129
  backend: string;
125
130
  };
131
+ /** A machine-home Channel-A request has no cloud/home lease. Its already
132
+ * established SelfhostedSession is pinned to the durable active pointer so
133
+ * the first command uses the exact machine instance and epoch. */
134
+ pinnedSelfhosted?: {
135
+ sandboxId: string;
136
+ epoch: number;
137
+ };
126
138
  directRequest: {
127
139
  requestId: string;
128
140
  holderId: string;
@@ -131,180 +143,212 @@ export function wrapChannelABoxWithRouting(
131
143
  established: EstablishedSandboxSession,
132
144
  ): EstablishedSandboxSession {
133
145
  const { db, settings, bus } = services;
134
- const beforeMutation = async ({
135
- op,
136
- backend,
137
- }: {
138
- op: string;
139
- backend: ResolvedActiveBackend;
140
- }): Promise<SandboxWorkspaceMutationAdmission | null> => {
141
- // Connected Machines and other non-persistable targets intentionally do
142
- // not dirty or advance the cloud-home archive generation.
143
- if (
144
- backend.sandboxId !== null ||
145
- backend.leaseEpoch === undefined ||
146
- backend.providerInstanceId === undefined
147
- ) {
148
- return null;
149
- }
150
- if (backend.activeEpoch === undefined) {
151
- throw new Error("API-direct workspace mutation resolved without an active route epoch");
152
- }
153
- return await advanceWorkspaceGenerationForDirectRequest(db, {
154
- accountId: ids.accountId,
155
- workspaceId: ids.workspaceId,
156
- sessionId: ids.sessionId,
157
- requestId: ids.directRequest.requestId,
158
- holderId: ids.directRequest.holderId,
159
- sandboxGroupId: ids.homeLease.sandboxGroupId,
160
- expectedEpoch: backend.leaseEpoch,
161
- expectedInstanceId: backend.providerInstanceId,
162
- routeTargetId: backend.sandboxId,
163
- routeEpoch: backend.activeEpoch,
164
- operation: op,
165
- });
166
- };
167
- const afterMutation = async ({
168
- op,
169
- backend,
170
- admission,
171
- outcome,
172
- retainedProcess,
173
- }: {
174
- op: string;
175
- backend: ResolvedActiveBackend;
176
- admission: unknown;
177
- outcome: "resolved" | "rejected";
178
- result?: unknown;
179
- retainedProcess?: RoutingRetainedProcess;
180
- }): Promise<void> => {
181
- if (admission === null) return;
182
- if (
183
- !admission ||
184
- typeof admission !== "object" ||
185
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
186
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
187
- "number" ||
188
- backend.leaseEpoch === undefined ||
189
- backend.providerInstanceId === undefined ||
190
- backend.activeEpoch === undefined
191
- ) {
192
- throw new Error("API-direct workspace mutation settlement lacked its exact admission");
193
- }
194
- const exactAdmission = admission as SandboxWorkspaceMutationAdmission;
195
- if (outcome === "resolved" && retainedProcess) {
196
- await retainWorkspaceMutationProcess(db, {
197
- accountId: ids.accountId,
198
- workspaceId: ids.workspaceId,
199
- sessionId: ids.sessionId,
200
- processId: retainedProcess.id,
201
- providerSessionId: retainedProcess.providerSessionId,
202
- admissionId: exactAdmission.id,
203
- admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
204
- operation: op,
205
- owner: {
206
- kind: "direct",
146
+ const homeLease = ids.homeLease;
147
+ const beforeMutation = homeLease
148
+ ? async ({
149
+ op,
150
+ backend,
151
+ }: {
152
+ op: string;
153
+ backend: ResolvedActiveBackend;
154
+ }): Promise<SandboxWorkspaceMutationAdmission | null> => {
155
+ // Connected Machines and other non-persistable targets intentionally do
156
+ // not dirty or advance the cloud-home archive generation.
157
+ if (
158
+ backend.sandboxId !== null ||
159
+ backend.leaseEpoch === undefined ||
160
+ backend.providerInstanceId === undefined
161
+ ) {
162
+ return null;
163
+ }
164
+ if (backend.activeEpoch === undefined) {
165
+ throw new Error("API-direct workspace mutation resolved without an active route epoch");
166
+ }
167
+ return await advanceWorkspaceGenerationForDirectRequest(db, {
168
+ accountId: ids.accountId,
169
+ workspaceId: ids.workspaceId,
170
+ sessionId: ids.sessionId,
171
+ requestId: ids.directRequest.requestId,
172
+ holderId: ids.directRequest.holderId,
173
+ sandboxGroupId: homeLease.sandboxGroupId,
174
+ expectedEpoch: backend.leaseEpoch,
175
+ expectedInstanceId: backend.providerInstanceId,
176
+ routeTargetId: backend.sandboxId,
177
+ routeEpoch: backend.activeEpoch,
178
+ operation: op,
179
+ });
180
+ }
181
+ : undefined;
182
+ const afterMutation = homeLease
183
+ ? async ({
184
+ op,
185
+ backend,
186
+ admission,
187
+ outcome,
188
+ retainedProcess,
189
+ }: {
190
+ op: string;
191
+ backend: ResolvedActiveBackend;
192
+ admission: unknown;
193
+ outcome: "resolved" | "rejected";
194
+ result?: unknown;
195
+ retainedProcess?: RoutingRetainedProcess;
196
+ }): Promise<void> => {
197
+ if (admission === null) return;
198
+ if (
199
+ !admission ||
200
+ typeof admission !== "object" ||
201
+ typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
202
+ typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
203
+ "number" ||
204
+ backend.leaseEpoch === undefined ||
205
+ backend.providerInstanceId === undefined ||
206
+ backend.activeEpoch === undefined
207
+ ) {
208
+ throw new Error("API-direct workspace mutation settlement lacked its exact admission");
209
+ }
210
+ const exactAdmission = admission as SandboxWorkspaceMutationAdmission;
211
+ if (outcome === "resolved" && retainedProcess) {
212
+ await retainWorkspaceMutationProcess(db, {
213
+ accountId: ids.accountId,
214
+ workspaceId: ids.workspaceId,
215
+ sessionId: ids.sessionId,
216
+ processId: retainedProcess.id,
217
+ providerSessionId: retainedProcess.providerSessionId,
218
+ admissionId: exactAdmission.id,
219
+ admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
220
+ operation: op,
221
+ owner: {
222
+ kind: "direct",
223
+ requestId: ids.directRequest.requestId,
224
+ holderId: ids.directRequest.holderId,
225
+ sandboxGroupId: homeLease.sandboxGroupId,
226
+ expectedEpoch: backend.leaseEpoch,
227
+ expectedInstanceId: backend.providerInstanceId,
228
+ routeTargetId: exactAdmission.routeTargetId,
229
+ routeEpoch: exactAdmission.routeEpoch,
230
+ },
231
+ });
232
+ return;
233
+ }
234
+ await verifyDirectWorkspaceMutationSettlement(db, {
235
+ accountId: ids.accountId,
236
+ workspaceId: ids.workspaceId,
237
+ sessionId: ids.sessionId,
207
238
  requestId: ids.directRequest.requestId,
208
239
  holderId: ids.directRequest.holderId,
209
- sandboxGroupId: ids.homeLease.sandboxGroupId,
240
+ sandboxGroupId: homeLease.sandboxGroupId,
210
241
  expectedEpoch: backend.leaseEpoch,
211
242
  expectedInstanceId: backend.providerInstanceId,
212
243
  routeTargetId: exactAdmission.routeTargetId,
213
244
  routeEpoch: exactAdmission.routeEpoch,
214
- },
215
- });
216
- return;
217
- }
218
- await verifyDirectWorkspaceMutationSettlement(db, {
219
- accountId: ids.accountId,
220
- workspaceId: ids.workspaceId,
221
- sessionId: ids.sessionId,
222
- requestId: ids.directRequest.requestId,
223
- holderId: ids.directRequest.holderId,
224
- sandboxGroupId: ids.homeLease.sandboxGroupId,
225
- expectedEpoch: backend.leaseEpoch,
226
- expectedInstanceId: backend.providerInstanceId,
227
- routeTargetId: exactAdmission.routeTargetId,
228
- routeEpoch: exactAdmission.routeEpoch,
229
- admission: exactAdmission,
230
- operation: op,
231
- outcome,
232
- });
233
- };
234
- const beforeProcessMutation = async ({
235
- op,
236
- process,
237
- }: {
238
- op: string;
239
- backend: ResolvedActiveBackend;
240
- process: RoutingRetainedProcess;
241
- }): Promise<SandboxWorkspaceMutationAdmission> =>
242
- await advanceWorkspaceGenerationForRetainedProcess(db, {
243
- accountId: ids.accountId,
244
- workspaceId: ids.workspaceId,
245
- sessionId: ids.sessionId,
246
- processId: process.id,
247
- operation: op,
248
- });
249
- const afterProcessMutation = async ({
250
- op,
251
- process,
252
- admission,
253
- outcome,
254
- }: {
255
- op: string;
256
- backend: ResolvedActiveBackend;
257
- process: RoutingRetainedProcess;
258
- admission: unknown;
259
- outcome: "resolved" | "rejected";
260
- result?: unknown;
261
- }): Promise<void> => {
262
- if (
263
- !admission ||
264
- typeof admission !== "object" ||
265
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
266
- typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
267
- "number"
268
- ) {
269
- throw new Error("API retained-process mutation settlement lacked its exact admission");
270
- }
271
- await verifyRetainedProcessMutationSettlement(db, {
272
- accountId: ids.accountId,
273
- workspaceId: ids.workspaceId,
274
- sessionId: ids.sessionId,
275
- processId: process.id,
276
- admission: admission as SandboxWorkspaceMutationAdmission,
277
- operation: op,
278
- outcome,
279
- });
280
- };
281
- const settleProcess = async ({
282
- backend,
283
- process,
284
- proof,
285
- }: {
286
- backend: ResolvedActiveBackend;
287
- process: RoutingRetainedProcess;
288
- proof: RoutingRetainedProcessTerminalProof;
289
- }): Promise<void> => {
290
- if (
291
- backend.sandboxId !== null ||
292
- backend.leaseEpoch === undefined ||
293
- backend.providerInstanceId === undefined
294
- ) {
295
- return;
296
- }
297
- await settleRetainedProcess(db, {
298
- accountId: ids.accountId,
299
- workspaceId: ids.workspaceId,
300
- sessionId: ids.sessionId,
301
- processId: process.id,
302
- outcome: proof.outcome,
303
- exitCode: proof.exitCode,
304
- reason: proof.reason,
305
- idleGraceMs: settings.sandboxIdleGraceMs,
306
- });
307
- };
245
+ admission: exactAdmission,
246
+ operation: op,
247
+ outcome,
248
+ });
249
+ }
250
+ : undefined;
251
+ const beforeProcessMutation = homeLease
252
+ ? async ({
253
+ op,
254
+ process,
255
+ }: {
256
+ op: string;
257
+ backend: ResolvedActiveBackend;
258
+ process: RoutingRetainedProcess;
259
+ }): Promise<SandboxWorkspaceMutationAdmission> =>
260
+ await advanceWorkspaceGenerationForRetainedProcess(db, {
261
+ accountId: ids.accountId,
262
+ workspaceId: ids.workspaceId,
263
+ sessionId: ids.sessionId,
264
+ processId: process.id,
265
+ operation: op,
266
+ })
267
+ : undefined;
268
+ const afterProcessMutation = homeLease
269
+ ? async ({
270
+ op,
271
+ process,
272
+ admission,
273
+ outcome,
274
+ }: {
275
+ op: string;
276
+ backend: ResolvedActiveBackend;
277
+ process: RoutingRetainedProcess;
278
+ admission: unknown;
279
+ outcome: "resolved" | "rejected";
280
+ result?: unknown;
281
+ }): Promise<void> => {
282
+ if (
283
+ !admission ||
284
+ typeof admission !== "object" ||
285
+ typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).id !== "string" ||
286
+ typeof (admission as Partial<SandboxWorkspaceMutationAdmission>).workspaceGeneration !==
287
+ "number"
288
+ ) {
289
+ throw new Error("API retained-process mutation settlement lacked its exact admission");
290
+ }
291
+ await verifyRetainedProcessMutationSettlement(db, {
292
+ accountId: ids.accountId,
293
+ workspaceId: ids.workspaceId,
294
+ sessionId: ids.sessionId,
295
+ processId: process.id,
296
+ admission: admission as SandboxWorkspaceMutationAdmission,
297
+ operation: op,
298
+ outcome,
299
+ });
300
+ }
301
+ : undefined;
302
+ const settleProcess = homeLease
303
+ ? async ({
304
+ backend,
305
+ process,
306
+ proof,
307
+ }: {
308
+ backend: ResolvedActiveBackend;
309
+ process: RoutingRetainedProcess;
310
+ proof: RoutingRetainedProcessTerminalProof;
311
+ }): Promise<void> => {
312
+ if (
313
+ backend.sandboxId !== null ||
314
+ backend.leaseEpoch === undefined ||
315
+ backend.providerInstanceId === undefined ||
316
+ backend.activeEpoch === undefined
317
+ ) {
318
+ return;
319
+ }
320
+ const durable = await getRetainedProcess(db, {
321
+ workspaceId: ids.workspaceId,
322
+ sessionId: ids.sessionId,
323
+ processId: process.id,
324
+ });
325
+ if (
326
+ !durable ||
327
+ durable.providerSessionId !== process.providerSessionId ||
328
+ durable.providerBackend !== backend.kind ||
329
+ durable.providerInstanceId !== backend.providerInstanceId ||
330
+ durable.leaseEpoch !== backend.leaseEpoch ||
331
+ durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
332
+ durable.routeTargetId !== backend.sandboxId ||
333
+ durable.routeEpoch !== backend.activeEpoch
334
+ ) {
335
+ throw new Error(
336
+ "API retained-process settlement lost its exact durable backend identity",
337
+ );
338
+ }
339
+ await settleRetainedProcess(db, {
340
+ accountId: ids.accountId,
341
+ workspaceId: ids.workspaceId,
342
+ sessionId: ids.sessionId,
343
+ processId: process.id,
344
+ expected: retainedProcessSettlementIdentity(durable),
345
+ outcome: proof.outcome,
346
+ exitCode: proof.exitCode,
347
+ reason: proof.reason,
348
+ idleGraceMs: settings.sandboxIdleGraceMs,
349
+ });
350
+ }
351
+ : undefined;
308
352
  const resolver = makeActiveBackendResolver({
309
353
  workspaceId: ids.workspaceId,
310
354
  defaultBackend: established.session as RoutableBackendSession,
@@ -322,13 +366,28 @@ export function wrapChannelABoxWithRouting(
322
366
  },
323
367
  controlRpcFactory: controlRpcFactory(bus),
324
368
  relay: relayConfigFromSettings(settings),
325
- resolveDefaultBackend: async () => ({
326
- session: established.session as RoutableBackendSession,
327
- sandboxId: null,
328
- kind: established.backendId,
329
- leaseEpoch: ids.homeLease.leaseEpoch,
330
- providerInstanceId: ids.homeLease.instanceId,
331
- }),
369
+ selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
370
+ selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
371
+ ...(ids.pinnedSelfhosted
372
+ ? {
373
+ pinnedSelfhosted: {
374
+ sandboxId: ids.pinnedSelfhosted.sandboxId,
375
+ epoch: ids.pinnedSelfhosted.epoch,
376
+ session: established.session as RoutableBackendSession,
377
+ },
378
+ }
379
+ : {}),
380
+ ...(homeLease
381
+ ? {
382
+ resolveDefaultBackend: async () => ({
383
+ session: established.session as RoutableBackendSession,
384
+ sandboxId: null,
385
+ kind: established.backendId,
386
+ leaseEpoch: homeLease.leaseEpoch,
387
+ providerInstanceId: homeLease.instanceId,
388
+ }),
389
+ }
390
+ : {}),
332
391
  });
333
392
 
334
393
  const proxy = new RoutingSandboxSession({
@@ -336,8 +395,12 @@ export function wrapChannelABoxWithRouting(
336
395
  session: established.session as RoutableBackendSession,
337
396
  sandboxId: null,
338
397
  kind: established.backendId,
339
- leaseEpoch: ids.homeLease.leaseEpoch,
340
- providerInstanceId: ids.homeLease.instanceId,
398
+ ...(homeLease
399
+ ? {
400
+ leaseEpoch: homeLease.leaseEpoch,
401
+ providerInstanceId: homeLease.instanceId,
402
+ }
403
+ : {}),
341
404
  },
342
405
  readPointer: async () => {
343
406
  if (!routingEnabled(settings)) {
@@ -347,43 +410,47 @@ export function wrapChannelABoxWithRouting(
347
410
  return pointer ?? { activeSandboxId: null, activeEpoch: 0 };
348
411
  },
349
412
  resolveActiveBackend: resolver,
350
- beforeMutation,
351
- afterMutation,
352
- beforeProcessMutation,
353
- afterProcessMutation,
354
- settleProcess,
355
- onDefaultBackendError: async ({ error }) => {
356
- if (!isProviderSandboxGoneDuringRoutedOperation(ids.homeLease.backend, error)) return null;
357
- const marked = await markWarmLeaseInstanceLost(db, {
358
- accountId: ids.accountId,
359
- workspaceId: ids.workspaceId,
360
- sandboxGroupId: ids.homeLease.sandboxGroupId,
361
- expectedEpoch: ids.homeLease.leaseEpoch,
362
- expectedInstanceId: ids.homeLease.instanceId,
363
- diagnostic: "provider_not_found_during_routed_operation",
364
- });
365
- if (marked.status === "marked" && bus) {
366
- await appendAndPublishEvents(db, bus, ids.workspaceId, ids.sessionId, [
367
- {
368
- type: "sandbox.box.lost",
369
- payload: { sandboxId: ids.homeLease.instanceId },
413
+ ...(beforeMutation ? { beforeMutation } : {}),
414
+ ...(afterMutation ? { afterMutation } : {}),
415
+ ...(beforeProcessMutation ? { beforeProcessMutation } : {}),
416
+ ...(afterProcessMutation ? { afterProcessMutation } : {}),
417
+ ...(settleProcess ? { settleProcess } : {}),
418
+ ...(homeLease
419
+ ? {
420
+ onDefaultBackendError: async ({ error }: { error: unknown }) => {
421
+ if (!isProviderSandboxGoneDuringRoutedOperation(homeLease.backend, error)) return null;
422
+ const marked = await markWarmLeaseInstanceLost(db, {
423
+ accountId: ids.accountId,
424
+ workspaceId: ids.workspaceId,
425
+ sandboxGroupId: homeLease.sandboxGroupId,
426
+ expectedEpoch: homeLease.leaseEpoch,
427
+ expectedInstanceId: homeLease.instanceId,
428
+ diagnostic: "provider_not_found_during_routed_operation",
429
+ });
430
+ if (marked.status === "marked" && bus) {
431
+ await appendAndPublishEvents(db, bus, ids.workspaceId, ids.sessionId, [
432
+ {
433
+ type: "sandbox.box.lost",
434
+ payload: { sandboxId: homeLease.instanceId },
435
+ },
436
+ ]).catch(() => undefined);
437
+ }
438
+ const lease = marked.lease;
439
+ const restore = lease?.recovery.restore.status;
440
+ return {
441
+ leaseEpoch: lease?.leaseEpoch ?? homeLease.leaseEpoch,
442
+ recovery:
443
+ marked.status === "stale"
444
+ ? ("superseded" as const)
445
+ : restore === "pending"
446
+ ? ("pending" as const)
447
+ : restore === "degraded"
448
+ ? ("degraded" as const)
449
+ : ("unrecoverable" as const),
450
+ };
370
451
  },
371
- ]).catch(() => undefined);
372
- }
373
- const lease = marked.lease;
374
- const restore = lease?.recovery.restore.status;
375
- return {
376
- leaseEpoch: lease?.leaseEpoch ?? ids.homeLease.leaseEpoch,
377
- recovery:
378
- marked.status === "stale"
379
- ? ("superseded" as const)
380
- : restore === "pending"
381
- ? ("pending" as const)
382
- : restore === "degraded"
383
- ? ("degraded" as const)
384
- : ("unrecoverable" as const),
385
- };
386
- },
452
+ }
453
+ : {}),
387
454
  });
388
455
 
389
456
  return { ...established, session: proxy };