@opengeni/db 1.0.1 → 3.2.0-canary.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 (271) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
  7. package/dist/chunk-2EH4WRHL.js.map +1 -0
  8. package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
  9. package/dist/chunk-6HMM2R5T.js +390 -0
  10. package/dist/chunk-6HMM2R5T.js.map +1 -0
  11. package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
  12. package/dist/chunk-AZHW7R5V.js.map +1 -0
  13. package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
  14. package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
  15. package/dist/chunk-KRLPEMLH.js.map +1 -0
  16. package/dist/chunk-NAB6FKFN.js +516 -0
  17. package/dist/chunk-NAB6FKFN.js.map +1 -0
  18. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  19. package/dist/chunk-SIBYBA2D.js.map +1 -0
  20. package/dist/chunk-UZNXG44K.js +4508 -0
  21. package/dist/chunk-UZNXG44K.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +85 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +23 -4
  35. package/dist/editable-artifact-durable-export.js +2 -2
  36. package/dist/editable-artifacts.d.ts +2 -2
  37. package/dist/editable-artifacts.js +3 -3
  38. package/dist/governed-learning-activation-schema.d.ts +984 -0
  39. package/dist/governed-learning-activation.d.ts +98 -0
  40. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  41. package/dist/governed-learning-evaluator.d.ts +28 -0
  42. package/dist/index.d.ts +1606 -81
  43. package/dist/index.js +21898 -8147
  44. package/dist/index.js.map +1 -1
  45. package/dist/insights.d.ts +18 -0
  46. package/dist/integration-bindings.d.ts +1 -0
  47. package/dist/integration-facets.d.ts +30 -0
  48. package/dist/interaction-placement-loss.d.ts +22 -0
  49. package/dist/managed-human-provisioning.d.ts +25 -0
  50. package/dist/memory-domain.d.ts +3 -3
  51. package/dist/migrate.d.ts +11 -1
  52. package/dist/migrate.js +1 -1
  53. package/dist/new-session-drafts.d.ts +43 -5
  54. package/dist/organization-membership-backfill.d.ts +174 -0
  55. package/dist/organization-membership-lifecycle.d.ts +162 -0
  56. package/dist/pr-review.d.ts +107 -0
  57. package/dist/preference-registry-schema.d.ts +2 -0
  58. package/dist/preference-registry.d.ts +2 -0
  59. package/dist/provision-roles.js +1 -1
  60. package/dist/runtime-posture.d.ts +29 -8
  61. package/dist/schema.d.ts +24914 -13703
  62. package/dist/schema.js +99 -1
  63. package/dist/scoped-knowledge-schema.d.ts +52 -1
  64. package/dist/session-background-commands.d.ts +125 -0
  65. package/dist/session-background-commands.js +34 -0
  66. package/dist/session-background-commands.js.map +1 -0
  67. package/dist/session-control.d.ts +156 -3
  68. package/dist/session-goal-pacing.d.ts +68 -0
  69. package/dist/session-queue-commands.d.ts +43 -4
  70. package/dist/session-realtime-ledger.d.ts +2 -0
  71. package/dist/session-tenancy.d.ts +71 -1
  72. package/dist/session-tenancy.js +25 -3
  73. package/dist/session-tool-call-settlement.d.ts +9 -4
  74. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  75. package/dist/slack-routing.d.ts +268 -0
  76. package/dist/slack-task-policy-schema.d.ts +34 -0
  77. package/dist/slack-task-policy.d.ts +8 -0
  78. package/dist/slack-user-link-access.d.ts +28 -0
  79. package/dist/task-notes-schema.d.ts +594 -0
  80. package/dist/task-notes.d.ts +39 -0
  81. package/dist/turn-initiator.d.ts +2 -0
  82. package/dist/user-resource-authority.d.ts +109 -0
  83. package/dist/video-generation.js +3 -3
  84. package/dist/workspace-authority.d.ts +81 -0
  85. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  86. package/dist/workspace-instruction-policies.d.ts +62 -0
  87. package/dist/workspace-learning-policy.d.ts +1 -0
  88. package/dist/workspace-membership-permissions.d.ts +2 -0
  89. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  90. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  91. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  92. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  93. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  94. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  95. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  96. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  97. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  98. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  99. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  100. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  101. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  102. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  103. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  104. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  105. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  106. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  107. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  108. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  109. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  110. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  111. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  112. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  113. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  114. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  115. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  116. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  117. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  118. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  119. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  120. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  121. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  122. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  123. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  124. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  125. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  126. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  127. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  128. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  129. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  130. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  131. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  132. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  133. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  134. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  135. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  136. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  137. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  138. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  139. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  140. package/drizzle/0294_preference_activation_authority.sql +237 -0
  141. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  142. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  143. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  144. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  145. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  146. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  147. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  148. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  149. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  150. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  151. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  152. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  153. package/drizzle/0307_session_attention_state.sql +51 -0
  154. package/drizzle/0308_session_archives.sql +35 -0
  155. package/drizzle/0309_channel_project_pins.sql +6 -0
  156. package/drizzle/0310_channel_project_order.sql +27 -0
  157. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  158. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  159. package/drizzle/0313_private_child_session_authority.sql +365 -0
  160. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  161. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  162. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  163. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  164. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  165. package/drizzle/0319_event_triggered_automations.sql +254 -0
  166. package/drizzle/0320_pr_review_pack.sql +113 -0
  167. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  168. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  169. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  170. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  171. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  172. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  173. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  174. package/drizzle/0328_session_background_commands.sql +590 -0
  175. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  176. package/drizzle/0330_api_key_descriptions.sql +20 -0
  177. package/drizzle/0331_managed_organization_creation.sql +163 -0
  178. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  179. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  180. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  181. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  182. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  183. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  184. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  185. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  186. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  187. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  188. package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
  189. package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
  190. package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
  191. package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
  192. package/drizzle/0346_document_migration_audit_surface.sql +454 -0
  193. package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
  194. package/package.json +10 -6
  195. package/src/attached-browser-devices.ts +233 -5
  196. package/src/automations.ts +911 -0
  197. package/src/browser-sessions.ts +10 -0
  198. package/src/capability-integrations.ts +175 -1
  199. package/src/child-lifecycle-notices.ts +241 -0
  200. package/src/codemode-operations.ts +1 -35
  201. package/src/company-brain-context-selection-schema.ts +100 -0
  202. package/src/company-brain-context-selection.ts +359 -0
  203. package/src/company-brain-governed-writes.ts +1170 -0
  204. package/src/company-brain.ts +309 -0
  205. package/src/company-profile-agent-admin.ts +225 -0
  206. package/src/company-profile-schema.ts +121 -1
  207. package/src/company-profile.ts +21 -0
  208. package/src/computer-sessions.ts +10 -1
  209. package/src/connection-authority.ts +290 -0
  210. package/src/connection-scopes.ts +20 -0
  211. package/src/connection-tenancy-backfill.ts +167 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +87 -2
  214. package/src/editable-artifacts-schema.ts +10 -5
  215. package/src/editable-artifacts.ts +21 -11
  216. package/src/governed-learning-activation-schema.ts +121 -0
  217. package/src/governed-learning-activation.ts +588 -0
  218. package/src/governed-learning-evaluator-schema.ts +72 -0
  219. package/src/governed-learning-evaluator.ts +217 -0
  220. package/src/index.ts +11676 -1550
  221. package/src/insights.ts +107 -0
  222. package/src/integration-bindings.ts +88 -6
  223. package/src/integration-facets.ts +287 -84
  224. package/src/interaction-placement-loss.ts +234 -0
  225. package/src/managed-human-provisioning.ts +116 -0
  226. package/src/memory-domain.ts +27 -32
  227. package/src/migrate.ts +77 -1
  228. package/src/new-session-drafts.ts +229 -35
  229. package/src/organization-membership-backfill.ts +735 -0
  230. package/src/organization-membership-lifecycle.ts +995 -0
  231. package/src/pr-review.ts +890 -0
  232. package/src/preference-registry.ts +31 -1
  233. package/src/provision-roles.ts +905 -6
  234. package/src/runtime-posture-cli.ts +2 -0
  235. package/src/runtime-posture.ts +1317 -72
  236. package/src/schema.ts +3316 -428
  237. package/src/scoped-knowledge-schema.ts +16 -8
  238. package/src/scoped-knowledge.ts +1 -1
  239. package/src/session-background-commands.ts +759 -0
  240. package/src/session-control.ts +640 -74
  241. package/src/session-goal-pacing.ts +126 -0
  242. package/src/session-queue-commands.ts +441 -85
  243. package/src/session-realtime-context.ts +4 -6
  244. package/src/session-realtime-ledger.ts +28 -14
  245. package/src/session-realtime-mirror.ts +1 -1
  246. package/src/session-tenancy.ts +410 -49
  247. package/src/session-tool-call-settlement.ts +38 -29
  248. package/src/slack-routing-personal-workspace.ts +67 -0
  249. package/src/slack-routing.ts +867 -0
  250. package/src/slack-task-policy-schema.ts +5 -0
  251. package/src/slack-task-policy.ts +18 -0
  252. package/src/slack-user-link-access.ts +138 -11
  253. package/src/task-notes-schema.ts +74 -0
  254. package/src/task-notes.ts +124 -2
  255. package/src/turn-initiator.ts +8 -0
  256. package/src/user-resource-authority.ts +377 -0
  257. package/src/workspace-authority.ts +160 -0
  258. package/src/workspace-instruction-policies-schema.ts +53 -1
  259. package/src/workspace-instruction-policies.ts +528 -77
  260. package/src/workspace-learning-policy.ts +21 -5
  261. package/src/workspace-membership-permissions.ts +9 -0
  262. package/dist/chunk-4SR4HKKA.js.map +0 -1
  263. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  264. package/dist/chunk-NXAFLCQX.js +0 -2468
  265. package/dist/chunk-NXAFLCQX.js.map +0 -1
  266. package/dist/chunk-SK7YYHBI.js.map +0 -1
  267. package/dist/chunk-WD5ROSLL.js +0 -117
  268. package/dist/chunk-WD5ROSLL.js.map +0 -1
  269. package/dist/chunk-ZWREYEWA.js.map +0 -1
  270. /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
  271. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
@@ -8,7 +8,7 @@ import {
8
8
  type AttachedBrowserDeviceListResponse as AttachedBrowserDeviceListResponseValue,
9
9
  type AttachedBrowserInventorySnapshot as AttachedBrowserInventorySnapshotValue,
10
10
  } from "@opengeni/contracts";
11
- import { and, asc, desc, eq, inArray, isNull } from "drizzle-orm";
11
+ import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm";
12
12
  import { type Database, withRlsContext } from "./database";
13
13
  import {
14
14
  advanceWorkspaceInteractionRevision,
@@ -16,6 +16,12 @@ import {
16
16
  } from "./interaction-revisions";
17
17
  import * as schema from "./schema";
18
18
 
19
+ const ATTACHED_DEVICE_LIVE_LIFECYCLES = ["starting", "active", "suspending", "restoring"] as const;
20
+ // `ending` stays off this list so an in-flight `/end` can finish physical
21
+ // teardown (`stopCapture` + helper exit) after Chrome reconnects.
22
+
23
+ const CONTROLLER_TRANSITION_EXPIRED = "controller_transition_expired";
24
+
19
25
  type DeviceRow = typeof schema.attachedBrowserDevices.$inferSelect;
20
26
 
21
27
  const CONSISTENT_READ = {
@@ -156,13 +162,29 @@ export async function reconcileAttachedBrowserInventory(
156
162
  .where(eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId))
157
163
  .for("update");
158
164
  const byId = new Map(rows.map((row) => [row.id, row]));
165
+ const generationChangedDeviceIds = new Set<string>();
159
166
  let changed = false;
160
167
  for (const device of snapshot.devices) {
161
168
  const existing = byId.get(device.id);
169
+ let reclaimingRevokedEnrollment = false;
162
170
  if (existing && existing.enrollmentId !== input.enrollmentId) {
163
- throw new AttachedBrowserInventoryConflictError(
164
- "attached browser device id is already owned by another enrollment",
165
- );
171
+ const [previousEnrollment] = await tx
172
+ .select({ status: schema.enrollments.status })
173
+ .from(schema.enrollments)
174
+ .where(
175
+ and(
176
+ eq(schema.enrollments.id, existing.enrollmentId),
177
+ eq(schema.enrollments.accountId, input.accountId),
178
+ eq(schema.enrollments.workspaceId, input.workspaceId),
179
+ ),
180
+ )
181
+ .limit(1);
182
+ if (previousEnrollment?.status !== "revoked") {
183
+ throw new AttachedBrowserInventoryConflictError(
184
+ "attached browser device id is already owned by another active enrollment",
185
+ );
186
+ }
187
+ reclaimingRevokedEnrollment = true;
166
188
  }
167
189
  if (!existing) {
168
190
  await tx.insert(schema.attachedBrowserDevices).values({
@@ -189,10 +211,15 @@ export async function reconcileAttachedBrowserInventory(
189
211
  changed = true;
190
212
  continue;
191
213
  }
192
- const materialChanged = !sameAnnouncement(existing, device);
214
+ if (existing.connectionGeneration !== device.connectionGeneration) {
215
+ generationChangedDeviceIds.add(device.id);
216
+ }
217
+ const materialChanged =
218
+ reclaimingRevokedEnrollment || !sameAnnouncement(existing, device);
193
219
  await tx
194
220
  .update(schema.attachedBrowserDevices)
195
221
  .set({
222
+ enrollmentId: input.enrollmentId,
196
223
  name: device.name,
197
224
  profileLabel: device.profileLabel,
198
225
  browserName: device.browserName,
@@ -217,6 +244,15 @@ export async function reconcileAttachedBrowserInventory(
217
244
  changed ||= materialChanged;
218
245
  }
219
246
 
247
+ if (
248
+ await terminalizeStaleAttachedDeviceSessions(tx, {
249
+ workspaceId: input.workspaceId,
250
+ deviceIds: [...generationChangedDeviceIds],
251
+ })
252
+ ) {
253
+ changed = true;
254
+ }
255
+
220
256
  const announced = new Set(snapshot.devices.map((device) => device.id));
221
257
  const disconnectedIds = rows
222
258
  .filter(
@@ -275,6 +311,153 @@ export async function reconcileAttachedBrowserInventory(
275
311
  );
276
312
  }
277
313
 
314
+ /** A new physical Chrome generation cannot inherit a prior controller token.
315
+ * Mark every still-live BrowserSession/ComputerSession on that exact device
316
+ * lost in place. Never rewrite placementInstanceId onto the successor. */
317
+ async function terminalizeStaleAttachedDeviceSessions(
318
+ tx: Database,
319
+ input: { workspaceId: string; deviceIds: readonly string[] },
320
+ ): Promise<boolean> {
321
+ if (input.deviceIds.length === 0) return false;
322
+
323
+ const browserRows = await tx
324
+ .select({ id: schema.browserSessions.id })
325
+ .from(schema.browserSessions)
326
+ .where(
327
+ and(
328
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
329
+ eq(schema.browserSessions.placementKind, "attached_device"),
330
+ inArray(schema.browserSessions.deviceId, [...input.deviceIds]),
331
+ inArray(schema.browserSessions.lifecycle, [...ATTACHED_DEVICE_LIVE_LIFECYCLES]),
332
+ ),
333
+ );
334
+ const computerRows = await tx
335
+ .select({ id: schema.computerSessions.id })
336
+ .from(schema.computerSessions)
337
+ .where(
338
+ and(
339
+ eq(schema.computerSessions.workspaceId, input.workspaceId),
340
+ eq(schema.computerSessions.placementKind, "attached_device"),
341
+ inArray(schema.computerSessions.deviceId, [...input.deviceIds]),
342
+ inArray(schema.computerSessions.lifecycle, [...ATTACHED_DEVICE_LIVE_LIFECYCLES]),
343
+ ),
344
+ );
345
+ if (browserRows.length === 0 && computerRows.length === 0) return false;
346
+
347
+ const browserIds = browserRows.map((row) => row.id).sort();
348
+ const computerIds = computerRows.map((row) => row.id).sort();
349
+ const resourceIds = [...browserIds, ...computerIds];
350
+ const operations = await tx
351
+ .select({ operationId: schema.interactionOperations.operationId })
352
+ .from(schema.interactionOperations)
353
+ .where(
354
+ and(
355
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
356
+ inArray(schema.interactionOperations.resourceId, resourceIds),
357
+ inArray(schema.interactionOperations.state, ["prepared", "dispatched"]),
358
+ ),
359
+ );
360
+ const operationIds = operations.map((row) => row.operationId).sort();
361
+ for (const operationId of operationIds) {
362
+ await tx.execute(sql`
363
+ select operation_id from interaction_operations
364
+ where workspace_id = ${input.workspaceId} and operation_id = ${operationId}
365
+ for update
366
+ `);
367
+ }
368
+ for (const browserSessionId of browserIds) {
369
+ await tx.execute(sql`
370
+ select id from browser_sessions
371
+ where workspace_id = ${input.workspaceId} and id = ${browserSessionId}
372
+ for update
373
+ `);
374
+ }
375
+ for (const computerSessionId of computerIds) {
376
+ await tx.execute(sql`
377
+ select id from computer_sessions
378
+ where workspace_id = ${input.workspaceId} and id = ${computerSessionId}
379
+ for update
380
+ `);
381
+ }
382
+
383
+ const now = new Date();
384
+ if (operationIds.length > 0) {
385
+ await tx
386
+ .update(schema.interactionOperations)
387
+ .set({
388
+ state: "outcome_unknown",
389
+ errorCode: "outcome_unknown",
390
+ errorMessage: "Attached Chrome connection generation changed",
391
+ errorRetryable: false,
392
+ errorDetails: { reason: "connection_generation_changed" },
393
+ settledAt: now,
394
+ updatedAt: now,
395
+ })
396
+ .where(
397
+ and(
398
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
399
+ inArray(schema.interactionOperations.operationId, operationIds),
400
+ inArray(schema.interactionOperations.state, ["prepared", "dispatched"]),
401
+ ),
402
+ );
403
+ }
404
+ if (browserIds.length > 0) {
405
+ await tx
406
+ .update(schema.browserSessions)
407
+ .set({
408
+ lifecycle: "lost",
409
+ failureCode: CONTROLLER_TRANSITION_EXPIRED,
410
+ updatedAt: now,
411
+ })
412
+ .where(
413
+ and(
414
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
415
+ inArray(schema.browserSessions.id, browserIds),
416
+ inArray(schema.browserSessions.lifecycle, [...ATTACHED_DEVICE_LIVE_LIFECYCLES]),
417
+ ),
418
+ );
419
+ }
420
+ if (computerIds.length > 0) {
421
+ await tx
422
+ .update(schema.computerSessions)
423
+ .set({
424
+ lifecycle: "lost",
425
+ failureCode: CONTROLLER_TRANSITION_EXPIRED,
426
+ updatedAt: now,
427
+ })
428
+ .where(
429
+ and(
430
+ eq(schema.computerSessions.workspaceId, input.workspaceId),
431
+ inArray(schema.computerSessions.id, computerIds),
432
+ inArray(schema.computerSessions.lifecycle, [...ATTACHED_DEVICE_LIVE_LIFECYCLES]),
433
+ ),
434
+ );
435
+ }
436
+ return true;
437
+ }
438
+
439
+ /** Lock the device row first (same order as inventory) and prove the live
440
+ * physical generation still matches this controller placement. */
441
+ export async function attachedDeviceGenerationMatches(
442
+ tx: Database,
443
+ input: { workspaceId: string; deviceId: string; placementInstanceId: string },
444
+ ): Promise<boolean> {
445
+ const [device] = await tx
446
+ .select({
447
+ connectionGeneration: schema.attachedBrowserDevices.connectionGeneration,
448
+ })
449
+ .from(schema.attachedBrowserDevices)
450
+ .where(
451
+ and(
452
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
453
+ eq(schema.attachedBrowserDevices.id, input.deviceId),
454
+ ),
455
+ )
456
+ .for("update")
457
+ .limit(1);
458
+ return device?.connectionGeneration === input.placementInstanceId;
459
+ }
460
+
278
461
  /** Immediately mark every live Chrome profile on a cleanly disconnected agent
279
462
  * offline. Historical endpoint rows remain discoverable and reconnect in place. */
280
463
  export async function disconnectAttachedBrowserDevices(
@@ -336,8 +519,53 @@ export async function listAttachedBrowserDevices(
336
519
  asc(schema.attachedBrowserDevices.name),
337
520
  asc(schema.attachedBrowserDevices.id),
338
521
  );
522
+ const inventories = await scopedDb
523
+ .select({
524
+ enrollmentId: schema.attachedBrowserInventories.enrollmentId,
525
+ bridgeGeneration: schema.attachedBrowserInventories.bridgeGeneration,
526
+ revision: schema.attachedBrowserInventories.revision,
527
+ observedAt: schema.attachedBrowserInventories.observedAt,
528
+ enrollmentStatus: schema.enrollments.status,
529
+ connectionLeaseExpiresAt: schema.enrollments.connectionLeaseExpiresAt,
530
+ wentOfflineAt: schema.enrollments.wentOfflineAt,
531
+ })
532
+ .from(schema.attachedBrowserInventories)
533
+ .innerJoin(
534
+ schema.enrollments,
535
+ and(
536
+ eq(schema.enrollments.id, schema.attachedBrowserInventories.enrollmentId),
537
+ eq(schema.enrollments.workspaceId, schema.attachedBrowserInventories.workspaceId),
538
+ ),
539
+ )
540
+ .where(eq(schema.attachedBrowserInventories.workspaceId, input.workspaceId))
541
+ .orderBy(
542
+ desc(schema.attachedBrowserInventories.observedAt),
543
+ asc(schema.attachedBrowserInventories.enrollmentId),
544
+ );
545
+ const connectedProfilesByEnrollment = new Map<string, number>();
546
+ for (const row of rows) {
547
+ if (row.disconnectedAt !== null) continue;
548
+ connectedProfilesByEnrollment.set(
549
+ row.enrollmentId,
550
+ (connectedProfilesByEnrollment.get(row.enrollmentId) ?? 0) + 1,
551
+ );
552
+ }
339
553
  return AttachedBrowserDeviceListResponse.parse({
340
554
  revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
555
+ bridges: inventories.map((inventory) => ({
556
+ enrollmentId: inventory.enrollmentId,
557
+ state:
558
+ inventory.enrollmentStatus === "active" &&
559
+ inventory.wentOfflineAt === null &&
560
+ inventory.connectionLeaseExpiresAt !== null &&
561
+ inventory.connectionLeaseExpiresAt.getTime() > Date.now()
562
+ ? "online"
563
+ : "offline",
564
+ bridgeGeneration: inventory.bridgeGeneration,
565
+ inventoryRevision: inventory.revision,
566
+ connectedProfileCount: connectedProfilesByEnrollment.get(inventory.enrollmentId) ?? 0,
567
+ lastSeenAt: inventory.observedAt.toISOString(),
568
+ })),
341
569
  devices: rows.map(deviceFromRow),
342
570
  });
343
571
  },