@opengeni/core 0.21.10 → 0.28.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.
Files changed (132) hide show
  1. package/dist/access/index.d.ts +1 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +2 -2
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/canonical-human-identities.d.ts +12 -0
  6. package/dist/canonical-human-identities.js +23 -0
  7. package/dist/canonical-human-identities.js.map +1 -0
  8. package/dist/chunk-6WKPWE5S.js +2380 -0
  9. package/dist/chunk-6WKPWE5S.js.map +1 -0
  10. package/dist/chunk-IBOEYG6N.js +34 -0
  11. package/dist/chunk-IBOEYG6N.js.map +1 -0
  12. package/dist/chunk-JJU6I5XD.js +2713 -0
  13. package/dist/chunk-JJU6I5XD.js.map +1 -0
  14. package/dist/chunk-NYPMQ7NO.js +2270 -0
  15. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  16. package/dist/dependencies.d.ts +30 -6
  17. package/dist/domain/capabilities.d.ts +28 -3
  18. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  19. package/dist/domain/conversation-integrations.d.ts +225 -0
  20. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  21. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  22. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  23. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  24. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  25. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  26. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  27. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  28. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  29. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  30. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  31. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  32. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  33. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  34. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  35. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  36. package/dist/domain/fiken.d.ts +35 -0
  37. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  38. package/dist/domain/packs.d.ts +29 -1
  39. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  40. package/dist/domain/scheduled-tasks.d.ts +2 -2
  41. package/dist/domain/session-tool-policy.d.ts +2 -2
  42. package/dist/domain/sessions.d.ts +24 -8
  43. package/dist/domain/skill-imports.d.ts +32 -0
  44. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  45. package/dist/domain/timeline-annotations.d.ts +5 -0
  46. package/dist/domain/video-generation-capabilities.d.ts +10 -0
  47. package/dist/domain/video-generation.d.ts +47 -0
  48. package/dist/editable-artifact-live/application.d.ts +69 -0
  49. package/dist/editable-artifact-live/errors.d.ts +1 -0
  50. package/dist/editable-artifact-live/index.d.ts +8 -0
  51. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  52. package/dist/editable-artifact-live/ports.d.ts +100 -0
  53. package/dist/editable-artifact-live/server.d.ts +71 -0
  54. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  55. package/dist/editable-artifact-live/types.d.ts +213 -0
  56. package/dist/editable-artifact-live/wire.d.ts +34 -0
  57. package/dist/editable-artifact-live.d.ts +2 -0
  58. package/dist/editable-artifact-live.js +58 -0
  59. package/dist/editable-artifact-live.js.map +1 -0
  60. package/dist/editable-artifacts.d.ts +2 -0
  61. package/dist/editable-artifacts.js +179 -0
  62. package/dist/editable-artifacts.js.map +1 -0
  63. package/dist/index.d.ts +37 -26
  64. package/dist/index.js +4145 -959
  65. package/dist/index.js.map +1 -1
  66. package/dist/managed-session.d.ts +6 -2
  67. package/dist/rigs/index.d.ts +1 -1
  68. package/dist/rigs/provider-images.d.ts +31 -0
  69. package/dist/sandbox/fleet.d.ts +8 -5
  70. package/dist/sandbox/routing.d.ts +1 -0
  71. package/dist/session-authorization.d.ts +4 -2
  72. package/dist/transcription.d.ts +5 -0
  73. package/dist/workflow-wake-contract.d.ts +7 -0
  74. package/package.json +22 -10
  75. package/src/access/index.ts +14 -2
  76. package/src/application/new-session-drafts.ts +2 -0
  77. package/src/application/session-commands.ts +188 -107
  78. package/src/canonical-human-identities.ts +27 -0
  79. package/src/dependencies.ts +34 -2
  80. package/src/domain/capabilities.ts +729 -408
  81. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  82. package/src/domain/conversation-integrations.ts +1238 -0
  83. package/src/domain/durable-learning-slack-publication.ts +202 -0
  84. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  85. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  86. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  87. package/src/domain/editable-artifacts/errors.ts +115 -0
  88. package/src/domain/editable-artifacts/hash.ts +131 -0
  89. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  90. package/src/domain/editable-artifacts/index.ts +14 -0
  91. package/src/domain/editable-artifacts/ports.ts +462 -0
  92. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  93. package/src/domain/editable-artifacts/service.ts +2402 -0
  94. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  95. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  96. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  97. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  98. package/src/domain/editable-artifacts/types.ts +835 -0
  99. package/src/domain/fiken.ts +88 -0
  100. package/src/domain/insights.ts +163 -50
  101. package/src/domain/memory-slack-delivery.ts +204 -0
  102. package/src/domain/memory-slack-publication.ts +5 -2
  103. package/src/domain/packs.ts +266 -5
  104. package/src/domain/personal-connection-delegations.ts +97 -6
  105. package/src/domain/scheduled-tasks.ts +171 -29
  106. package/src/domain/session-tool-policy.ts +8 -2
  107. package/src/domain/sessions.ts +395 -148
  108. package/src/domain/skill-imports.ts +350 -0
  109. package/src/domain/slack-publication-secret-safety.ts +42 -0
  110. package/src/domain/timeline-annotations.ts +204 -0
  111. package/src/domain/video-generation-capabilities.ts +104 -0
  112. package/src/domain/video-generation.ts +249 -0
  113. package/src/editable-artifact-live/application.ts +182 -0
  114. package/src/editable-artifact-live/errors.ts +4 -0
  115. package/src/editable-artifact-live/index.ts +8 -0
  116. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  117. package/src/editable-artifact-live/ports.ts +139 -0
  118. package/src/editable-artifact-live/server.ts +1707 -0
  119. package/src/editable-artifact-live/ticket.ts +217 -0
  120. package/src/editable-artifact-live/types.ts +286 -0
  121. package/src/editable-artifact-live/wire.ts +95 -0
  122. package/src/editable-artifact-live.ts +2 -0
  123. package/src/editable-artifacts.ts +2 -0
  124. package/src/index.ts +15 -0
  125. package/src/managed-session.ts +19 -2
  126. package/src/rigs/index.ts +26 -19
  127. package/src/rigs/provider-images.ts +110 -0
  128. package/src/sandbox/fleet.ts +72 -66
  129. package/src/sandbox/routing.ts +6 -3
  130. package/src/session-authorization.ts +187 -52
  131. package/src/transcription.ts +5 -0
  132. package/src/workflow-wake-contract.ts +8 -0
package/src/rigs/index.ts CHANGED
@@ -36,6 +36,9 @@ import {
36
36
  type Database,
37
37
  } from "@opengeni/db";
38
38
  import { HTTPException } from "hono/http-exception";
39
+ import { rigProviderImagesFromVerification } from "./provider-images";
40
+
41
+ export * from "./provider-images";
39
42
 
40
43
  export const MAX_RIGS_PER_WORKSPACE = 50;
41
44
  export const MAX_CHECKS_PER_RIG = 100;
@@ -352,6 +355,13 @@ export async function promoteSetupAppendChange(
352
355
  if (typeof payload.command !== "string" || !payload.command.trim()) {
353
356
  throw new HTTPException(422, { message: "setup_append change is missing command" });
354
357
  }
358
+ const nextDefinition = {
359
+ image: base.image,
360
+ setupScript: appendRigSetupCommand(base.setupScript, payload.command),
361
+ checks: base.checks,
362
+ credentialHooks: base.credentialHooks,
363
+ defaultVariableSetIds: base.defaultVariableSetIds,
364
+ };
355
365
  const { version, change: updated } = await promoteChangeWithActiveCas(
356
366
  deps,
357
367
  grant.workspaceId,
@@ -359,16 +369,13 @@ export async function promoteSetupAppendChange(
359
369
  change.id,
360
370
  {
361
371
  expectedActiveVersionId: change.baseVersionId,
362
- image: base.image,
363
- setupScript: appendRigSetupCommand(base.setupScript, payload.command),
364
- checks: base.checks,
365
- credentialHooks: base.credentialHooks,
366
- defaultVariableSetIds: base.defaultVariableSetIds,
372
+ ...nextDefinition,
367
373
  changelog:
368
374
  typeof payload.note === "string" && payload.note.trim()
369
375
  ? payload.note
370
376
  : "Verified setup append",
371
377
  createdBy: change.proposedBy ?? rigActorForGrant(grant),
378
+ providerImages: rigProviderImagesFromVerification(change.verification, nextDefinition),
372
379
  },
373
380
  );
374
381
  await recordRigAuditEvent(deps.db, {
@@ -418,6 +425,18 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
418
425
  defaultVariableSetIds?: unknown;
419
426
  changelog?: unknown;
420
427
  };
428
+ const nextDefinition = {
429
+ image: payload.image === undefined ? base.image : (payload.image as string | null),
430
+ setupScript:
431
+ payload.setupScript === undefined ? base.setupScript : (payload.setupScript as string | null),
432
+ checks: Array.isArray(payload.checks) ? (payload.checks as RigVersion["checks"]) : base.checks,
433
+ credentialHooks: Array.isArray(payload.credentialHooks)
434
+ ? (payload.credentialHooks as string[])
435
+ : base.credentialHooks,
436
+ defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds)
437
+ ? (payload.defaultVariableSetIds as string[])
438
+ : base.defaultVariableSetIds,
439
+ };
421
440
  const { version, change: updated } = await promoteChangeWithActiveCas(
422
441
  deps,
423
442
  grant.workspaceId,
@@ -425,25 +444,13 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
425
444
  change.id,
426
445
  {
427
446
  expectedActiveVersionId: change.baseVersionId,
428
- image: payload.image === undefined ? base.image : (payload.image as string | null),
429
- setupScript:
430
- payload.setupScript === undefined
431
- ? base.setupScript
432
- : (payload.setupScript as string | null),
433
- checks: Array.isArray(payload.checks)
434
- ? (payload.checks as RigVersion["checks"])
435
- : base.checks,
436
- credentialHooks: Array.isArray(payload.credentialHooks)
437
- ? (payload.credentialHooks as string[])
438
- : base.credentialHooks,
439
- defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds)
440
- ? (payload.defaultVariableSetIds as string[])
441
- : base.defaultVariableSetIds,
447
+ ...nextDefinition,
442
448
  changelog:
443
449
  typeof payload.changelog === "string" && payload.changelog.trim()
444
450
  ? payload.changelog
445
451
  : "Verified definition edit",
446
452
  createdBy: rigActorForGrant(grant),
453
+ providerImages: rigProviderImagesFromVerification(change.verification, nextDefinition),
447
454
  },
448
455
  );
449
456
  await recordRigAuditEvent(deps.db, {
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ RigProviderImage as RigProviderImageContract,
4
+ stableJson,
5
+ type RigChangeVerification,
6
+ type RigProviderImage,
7
+ type RigProviderImages,
8
+ type RigVersion,
9
+ type SandboxBackend,
10
+ } from "@opengeni/contracts";
11
+
12
+ export type RigProviderImageDefinition = Pick<
13
+ RigVersion,
14
+ "image" | "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
15
+ >;
16
+
17
+ function sha256(value: string): string {
18
+ return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
19
+ }
20
+
21
+ export function rigProviderImageProviderBindingKeyHash(bindingKey: string): string {
22
+ return sha256(bindingKey);
23
+ }
24
+
25
+ export function rigProviderImageSetupHash(
26
+ definition: Pick<RigProviderImageDefinition, "setupScript">,
27
+ ): string {
28
+ return sha256(definition.setupScript ?? "");
29
+ }
30
+
31
+ /**
32
+ * Hash every exact rig-definition input that can affect whether a provider
33
+ * image may be reused. Credential values and variable-set contents are
34
+ * intentionally absent: verification never injects them into the build box.
35
+ * Their declared hook/set identities still participate so a new rig version
36
+ * cannot silently inherit an older build record.
37
+ */
38
+ export function rigProviderImageContentHash(input: {
39
+ backend: SandboxBackend;
40
+ sourceImage: string | null;
41
+ definition: RigProviderImageDefinition;
42
+ }): string {
43
+ return sha256(
44
+ stableJson({
45
+ version: 1,
46
+ backend: input.backend,
47
+ sourceImage: input.sourceImage,
48
+ image: input.definition.image,
49
+ setupScript: input.definition.setupScript,
50
+ checks: input.definition.checks,
51
+ credentialHooks: input.definition.credentialHooks,
52
+ defaultVariableSetIds: input.definition.defaultVariableSetIds,
53
+ }),
54
+ );
55
+ }
56
+
57
+ /** Modal's patched snapshot API accepts a caller-owned UUID idempotency key.
58
+ * v2 denotes images proven by a separate cold boot before publication. */
59
+ export function rigProviderImageBuildRequestId(input: {
60
+ targetId: string;
61
+ backend: SandboxBackend;
62
+ contentHash: string;
63
+ }): string {
64
+ const bytes = createHash("sha256")
65
+ .update("opengeni-rig-provider-image-build-v2\0", "utf8")
66
+ .update(input.targetId, "utf8")
67
+ .update("\0", "utf8")
68
+ .update(input.backend, "utf8")
69
+ .update("\0", "utf8")
70
+ .update(input.contentHash, "utf8")
71
+ .digest()
72
+ .subarray(0, 16);
73
+ // RFC 4122 variant + deterministic version-5-shaped UUID. The digest, not a
74
+ // provider response, is the idempotency identity.
75
+ bytes[6] = (bytes[6]! & 0x0f) | 0x50;
76
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
77
+ const hex = bytes.toString("hex");
78
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
79
+ }
80
+
81
+ export function rigProviderImageMatchesDefinition(
82
+ image: RigProviderImage,
83
+ definition: RigProviderImageDefinition,
84
+ ): boolean {
85
+ return (
86
+ image.setupHash === rigProviderImageSetupHash(definition) &&
87
+ image.contentHash ===
88
+ rigProviderImageContentHash({
89
+ backend: image.backend,
90
+ sourceImage: image.sourceImage,
91
+ definition,
92
+ })
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Promotion copies only a finalized, structurally valid build record whose
98
+ * hashes match the exact version definition being inserted. A malformed or
99
+ * stale optimization record is discarded; the version remains correct and
100
+ * falls back to runtime setup.
101
+ */
102
+ export function rigProviderImagesFromVerification(
103
+ verification: RigChangeVerification | null,
104
+ definition: RigProviderImageDefinition,
105
+ ): RigProviderImages {
106
+ const parsed = RigProviderImageContract.safeParse(verification?.providerImage);
107
+ if (!parsed.success || parsed.data.status === "building") return {};
108
+ if (!rigProviderImageMatchesDefinition(parsed.data, definition)) return {};
109
+ return { [parsed.data.backend]: parsed.data };
110
+ }
@@ -37,7 +37,6 @@ import {
37
37
  type SelfhostedRelayConfig,
38
38
  type SelfhostedOpStreamDeps,
39
39
  } from "@opengeni/runtime/sandbox";
40
- import { HTTPException } from "hono/http-exception";
41
40
  import { relayConfigFromSettings } from "./routing";
42
41
 
43
42
  export type FleetServices = {
@@ -69,8 +68,9 @@ export type FleetContext = {
69
68
 
70
69
  /**
71
70
  * Build a session-scoped {@link FleetContext}: load the session (workspace-
72
- * scoped), reject a session with no box (backend:none the fleet is only
73
- * meaningful for a sandboxed session), and project its group backend/id. Shared
71
+ * scoped) and project its group backend/id. A backend:none session has no home
72
+ * box, but it may still discover, run on, and attach an owned Connected Machine.
73
+ * Shared
74
74
  * by the worker-signed MCP fleet tools and the user-authenticated swap REST
75
75
  * route so both resolve the SAME context (no drift). The `accountId`/`workspaceId`/
76
76
  * `sessionId` come from the trusted grant/route; the backend + group id come from
@@ -81,11 +81,6 @@ export async function buildFleetContextForSession(
81
81
  ctx: { accountId: string; workspaceId: string; sessionId: string },
82
82
  ): Promise<FleetContext> {
83
83
  const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
84
- if (session.sandboxBackend === "none") {
85
- throw new HTTPException(422, {
86
- message: "this session has no sandbox (backend: none); the fleet is unavailable",
87
- });
88
- }
89
84
  return {
90
85
  accountId: ctx.accountId,
91
86
  workspaceId: ctx.workspaceId,
@@ -229,9 +224,11 @@ async function probeEnrollment(
229
224
  }
230
225
 
231
226
  /**
232
- * List the fleet: the session's own Modal group box (a synthetic entry) + the
233
- * workspace's first-class selfhosted sandboxes (each probed for liveness), each
234
- * with an `active` marker derived from the session's active pointer.
227
+ * List the fleet: the session's own group box when it has one (a synthetic
228
+ * entry) + the workspace's first-class selfhosted sandboxes (each probed for
229
+ * liveness), each with an `active` marker derived from the session's active
230
+ * pointer. A backend:none session has no synthetic home entry; a null pointer
231
+ * then means no compute is attached.
235
232
  */
236
233
  export async function listFleet(
237
234
  services: FleetServices,
@@ -245,61 +242,63 @@ export async function listFleet(
245
242
 
246
243
  const entries: FleetSandboxEntry[] = [];
247
244
 
248
- // The session's own group box (the default/home sandbox; null active pointer ==
249
- // this box). A session/group row is not provider existence. Online requires a
250
- // warm lease, observed provider existence, and verified workspace readiness.
251
- const groupActive = pointer.activeSandboxId === null;
252
- const groupLease = await readLease(db, ctx.workspaceId, ctx.sessionGroupId);
253
- const groupOnline = Boolean(
254
- groupLease?.liveness === "warm" &&
255
- groupLease.recovery.provider.status === "exists" &&
256
- groupLease.recovery.workspace.status === "ready",
257
- );
258
- const groupRecovering = Boolean(
259
- groupLease &&
260
- (groupLease.liveness === "warming" ||
261
- groupLease.recovery.restore.status === "pending" ||
262
- groupLease.recovery.restore.status === "restoring" ||
263
- groupLease.recovery.restore.status === "verifying"),
264
- );
265
- const groupRecoveryUnavailable = Boolean(
266
- groupLease &&
267
- (groupLease.recovery.restore.status === "degraded" ||
268
- groupLease.recovery.restore.status === "unrecoverable" ||
269
- groupLease.recovery.workspace.status === "degraded" ||
270
- groupLease.recovery.workspace.status === "unrecoverable"),
271
- );
272
- const groupOperationAvailability: FleetOperationAvailability = groupOnline
273
- ? "ready"
274
- : groupRecoveryUnavailable
275
- ? "unavailable"
276
- : groupRecovering
277
- ? "recovering"
278
- : ctx.sessionBackend === "selfhosted"
279
- ? "unavailable"
280
- : "wakeable";
281
- entries.push({
282
- id: ctx.sessionGroupId,
283
- kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
284
- name: "session sandbox",
285
- liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
286
- active: groupActive,
287
- isSessionGroup: true,
288
- enrollmentId: null,
289
- attachable: groupOnline,
290
- operationAvailability: groupOperationAvailability,
291
- providerStatus: groupLease?.recovery.provider.status ?? "not_created",
292
- leaseLiveness: groupLease?.liveness ?? null,
293
- routeStatus: groupActive ? "attached" : "detached",
294
- archiveStatus: groupLease?.recovery.archive.status ?? "none",
295
- restoreStatus: groupLease?.recovery.restore.status ?? "not_required",
296
- workspaceStatus: groupLease?.recovery.workspace.status ?? "unknown",
297
- leaseEpoch: groupLease?.leaseEpoch ?? null,
298
- routeEpoch: pointer.activeEpoch,
299
- workspaceGeneration: groupLease?.workspaceGeneration ?? null,
300
- archiveGeneration: groupLease?.archiveGeneration ?? null,
301
- archiveComplete: groupLease?.archiveComplete ?? false,
302
- });
245
+ if (ctx.sessionBackend !== "none") {
246
+ // The session's own group box (the default/home sandbox; null active pointer ==
247
+ // this box). A session/group row is not provider existence. Online requires a
248
+ // warm lease, observed provider existence, and verified workspace readiness.
249
+ const groupActive = pointer.activeSandboxId === null;
250
+ const groupLease = await readLease(db, ctx.workspaceId, ctx.sessionGroupId);
251
+ const groupOnline = Boolean(
252
+ groupLease?.liveness === "warm" &&
253
+ groupLease.recovery.provider.status === "exists" &&
254
+ groupLease.recovery.workspace.status === "ready",
255
+ );
256
+ const groupRecovering = Boolean(
257
+ groupLease &&
258
+ (groupLease.liveness === "warming" ||
259
+ groupLease.recovery.restore.status === "pending" ||
260
+ groupLease.recovery.restore.status === "restoring" ||
261
+ groupLease.recovery.restore.status === "verifying"),
262
+ );
263
+ const groupRecoveryUnavailable = Boolean(
264
+ groupLease &&
265
+ (groupLease.recovery.restore.status === "degraded" ||
266
+ groupLease.recovery.restore.status === "unrecoverable" ||
267
+ groupLease.recovery.workspace.status === "degraded" ||
268
+ groupLease.recovery.workspace.status === "unrecoverable"),
269
+ );
270
+ const groupOperationAvailability: FleetOperationAvailability = groupOnline
271
+ ? "ready"
272
+ : groupRecoveryUnavailable
273
+ ? "unavailable"
274
+ : groupRecovering
275
+ ? "recovering"
276
+ : ctx.sessionBackend === "selfhosted"
277
+ ? "unavailable"
278
+ : "wakeable";
279
+ entries.push({
280
+ id: ctx.sessionGroupId,
281
+ kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
282
+ name: "session sandbox",
283
+ liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
284
+ active: groupActive,
285
+ isSessionGroup: true,
286
+ enrollmentId: null,
287
+ attachable: groupOnline,
288
+ operationAvailability: groupOperationAvailability,
289
+ providerStatus: groupLease?.recovery.provider.status ?? "not_created",
290
+ leaseLiveness: groupLease?.liveness ?? null,
291
+ routeStatus: groupActive ? "attached" : "detached",
292
+ archiveStatus: groupLease?.recovery.archive.status ?? "none",
293
+ restoreStatus: groupLease?.recovery.restore.status ?? "not_required",
294
+ workspaceStatus: groupLease?.recovery.workspace.status ?? "unknown",
295
+ leaseEpoch: groupLease?.leaseEpoch ?? null,
296
+ routeEpoch: pointer.activeEpoch,
297
+ workspaceGeneration: groupLease?.workspaceGeneration ?? null,
298
+ archiveGeneration: groupLease?.archiveGeneration ?? null,
299
+ archiveComplete: groupLease?.archiveComplete ?? false,
300
+ });
301
+ }
303
302
 
304
303
  // The workspace's first-class selfhosted sandboxes (enrolled machines). Probe
305
304
  // each for liveness; a missing enrollment is offline.
@@ -374,6 +373,13 @@ async function resolveTarget(
374
373
  > {
375
374
  // The session's own group box → the default pointer (null).
376
375
  if (target === ctx.sessionGroupId || target === "session" || target === "default") {
376
+ if (ctx.sessionBackend === "none") {
377
+ return {
378
+ ok: false,
379
+ reason: "this session has no home sandbox; attach a Connected Machine",
380
+ code: "unsupported_backend_context",
381
+ };
382
+ }
377
383
  return { ok: true, targetSandboxId: null };
378
384
  }
379
385
  const sandbox = await getSandbox(services.db, ctx.workspaceId, target);
@@ -11,7 +11,7 @@
11
11
  // The DB-coupled glue (readActiveSandbox / getSandbox / the selfhosted ControlRpc
12
12
  // over the events bus) lives here, not in the leaf (which stays db-free).
13
13
 
14
- import { sandboxArchiveCaptureTimeoutMs, type Settings } from "@opengeni/config";
14
+ import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/config";
15
15
  import {
16
16
  advanceWorkspaceGenerationForDirectRequest,
17
17
  advanceWorkspaceGenerationForRetainedProcess,
@@ -61,6 +61,7 @@ export type ChannelARoutingServices = {
61
61
  settings: Settings;
62
62
  bus?: EventBus;
63
63
  onSandboxOperation?: RoutingSandboxOperationObserver;
64
+ waitSignal?: AbortSignal;
64
65
  };
65
66
 
66
67
  /** Map the deployment relay URL to the leaf's `SelfhostedRelayConfig` shape. The
@@ -214,7 +215,8 @@ export function wrapChannelABoxWithRouting(
214
215
  routeTargetId: backend.sandboxId,
215
216
  routeEpoch: backend.activeEpoch,
216
217
  operation: op,
217
- captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
218
+ captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
219
+ ...(services.waitSignal ? { waitSignal: services.waitSignal } : {}),
218
220
  });
219
221
  return { admission, providerBinding };
220
222
  }
@@ -310,7 +312,8 @@ export function wrapChannelABoxWithRouting(
310
312
  sessionId: ids.sessionId,
311
313
  processId: process.id,
312
314
  operation: op,
313
- captureWaitMs: sandboxArchiveCaptureTimeoutMs(settings),
315
+ captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
316
+ ...(services.waitSignal ? { waitSignal: services.waitSignal } : {}),
314
317
  })
315
318
  : undefined;
316
319
  const afterProcessMutation = homeLease