@opengeni/db 0.28.9 → 0.36.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 (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.28.9",
3
+ "version": "0.36.0",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -34,6 +34,26 @@
34
34
  "./provision-roles": {
35
35
  "types": "./dist/provision-roles.d.ts",
36
36
  "import": "./dist/provision-roles.js"
37
+ },
38
+ "./editable-artifacts": {
39
+ "types": "./dist/editable-artifacts.d.ts",
40
+ "import": "./dist/editable-artifacts.js"
41
+ },
42
+ "./editable-artifact-durable-export": {
43
+ "types": "./dist/editable-artifact-durable-export.d.ts",
44
+ "import": "./dist/editable-artifact-durable-export.js"
45
+ },
46
+ "./video-generation": {
47
+ "types": "./dist/video-generation.d.ts",
48
+ "import": "./dist/video-generation.js"
49
+ },
50
+ "./canonical-human-identities": {
51
+ "types": "./dist/canonical-human-identities.d.ts",
52
+ "import": "./dist/canonical-human-identities.js"
53
+ },
54
+ "./session-tenancy": {
55
+ "types": "./dist/session-tenancy.d.ts",
56
+ "import": "./dist/session-tenancy.js"
37
57
  }
38
58
  },
39
59
  "publishConfig": {
@@ -50,10 +70,11 @@
50
70
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
51
71
  },
52
72
  "dependencies": {
53
- "@opengeni/codex": "^0.2.13",
54
- "@opengeni/config": "^0.12.1",
55
- "@opengeni/contracts": "^0.40.0",
56
- "@opengeni/network": "^0.2.0",
73
+ "@opengeni/codemode": "^0.4.1",
74
+ "@opengeni/codex": "^0.2.16",
75
+ "@opengeni/config": "^0.16.1",
76
+ "@opengeni/contracts": "^0.50.0",
77
+ "@opengeni/network": "^0.2.2",
57
78
  "drizzle-orm": "^0.45.2",
58
79
  "postgres": "^3.4.7"
59
80
  },
@@ -0,0 +1,371 @@
1
+ import {
2
+ AttachedBrowserDevice,
3
+ AttachedBrowserDeviceCapabilities,
4
+ AttachedBrowserDeviceListResponse,
5
+ AttachedBrowserInventorySnapshot,
6
+ type AttachedBrowserDevice as AttachedBrowserDeviceValue,
7
+ type AttachedBrowserDeviceAnnouncement,
8
+ type AttachedBrowserDeviceListResponse as AttachedBrowserDeviceListResponseValue,
9
+ type AttachedBrowserInventorySnapshot as AttachedBrowserInventorySnapshotValue,
10
+ } from "@opengeni/contracts";
11
+ import { and, asc, desc, eq, inArray, isNull } from "drizzle-orm";
12
+ import { type Database, withRlsContext } from "./database";
13
+ import {
14
+ advanceWorkspaceInteractionRevision,
15
+ readWorkspaceInteractionRevision,
16
+ } from "./interaction-revisions";
17
+ import * as schema from "./schema";
18
+
19
+ type DeviceRow = typeof schema.attachedBrowserDevices.$inferSelect;
20
+
21
+ const CONSISTENT_READ = {
22
+ isolationLevel: "repeatable read",
23
+ accessMode: "read only",
24
+ } as const;
25
+
26
+ export class AttachedBrowserDeviceNotFoundError extends Error {
27
+ readonly name = "AttachedBrowserDeviceNotFoundError";
28
+ }
29
+
30
+ export class AttachedBrowserInventoryConflictError extends Error {
31
+ readonly name = "AttachedBrowserInventoryConflictError";
32
+ }
33
+
34
+ export type ReconcileAttachedBrowserInventoryResult = {
35
+ accepted: boolean;
36
+ changed: boolean;
37
+ revision: number;
38
+ };
39
+
40
+ function safeInteger(value: number, label: string): number {
41
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is outside safe range`);
42
+ return value;
43
+ }
44
+
45
+ function deviceFromRow(row: DeviceRow): AttachedBrowserDeviceValue {
46
+ return AttachedBrowserDevice.parse({
47
+ id: row.id,
48
+ accountId: row.accountId,
49
+ workspaceId: row.workspaceId,
50
+ enrollmentId: row.enrollmentId,
51
+ name: row.name,
52
+ profileLabel: row.profileLabel,
53
+ browserName: row.browserName,
54
+ browserVersion: row.browserVersion,
55
+ extensionVersion: row.extensionVersion,
56
+ platform: row.platform,
57
+ architecture: row.architecture,
58
+ state: row.disconnectedAt ? "disconnected" : "connected",
59
+ connectionGeneration: row.connectionGeneration,
60
+ inventoryRevision: safeInteger(row.inventoryRevision, "attached browser inventory revision"),
61
+ tabCount: safeInteger(row.tabCount, "attached browser tab count"),
62
+ capabilities: row.capabilities,
63
+ lastSeenAt: row.lastSeenAt.toISOString(),
64
+ disconnectedAt: row.disconnectedAt?.toISOString() ?? null,
65
+ createdAt: row.createdAt.toISOString(),
66
+ updatedAt: row.updatedAt.toISOString(),
67
+ });
68
+ }
69
+
70
+ function sameAnnouncement(row: DeviceRow, value: AttachedBrowserDeviceAnnouncement): boolean {
71
+ return (
72
+ row.name === value.name &&
73
+ row.profileLabel === value.profileLabel &&
74
+ row.browserName === value.browserName &&
75
+ row.browserVersion === value.browserVersion &&
76
+ row.extensionVersion === value.extensionVersion &&
77
+ row.platform === value.platform &&
78
+ row.architecture === value.architecture &&
79
+ row.connectionGeneration === value.connectionGeneration &&
80
+ row.inventoryRevision === value.inventoryRevision &&
81
+ row.tabCount === value.tabCount &&
82
+ JSON.stringify(AttachedBrowserDeviceCapabilities.parse(row.capabilities)) ===
83
+ JSON.stringify(value.capabilities) &&
84
+ row.disconnectedAt === null
85
+ );
86
+ }
87
+
88
+ /** Reconcile one full browser-bridge snapshot. The enrollment row serializes
89
+ * concurrent heartbeats; a lower revision from the same bridge generation is
90
+ * ignored, and a replay only refreshes liveness. */
91
+ export async function reconcileAttachedBrowserInventory(
92
+ db: Database,
93
+ input: {
94
+ accountId: string;
95
+ workspaceId: string;
96
+ enrollmentId: string;
97
+ snapshot: AttachedBrowserInventorySnapshotValue;
98
+ },
99
+ ): Promise<ReconcileAttachedBrowserInventoryResult> {
100
+ const snapshot = AttachedBrowserInventorySnapshot.parse(input.snapshot);
101
+ return await withRlsContext(
102
+ db,
103
+ input,
104
+ async (scopedDb) =>
105
+ await scopedDb.transaction(async (rawTx) => {
106
+ const tx = rawTx as unknown as Database;
107
+ const [enrollment] = await tx
108
+ .select({
109
+ id: schema.enrollments.id,
110
+ accountId: schema.enrollments.accountId,
111
+ workspaceId: schema.enrollments.workspaceId,
112
+ status: schema.enrollments.status,
113
+ })
114
+ .from(schema.enrollments)
115
+ .where(
116
+ and(
117
+ eq(schema.enrollments.id, input.enrollmentId),
118
+ eq(schema.enrollments.accountId, input.accountId),
119
+ eq(schema.enrollments.workspaceId, input.workspaceId),
120
+ ),
121
+ )
122
+ .for("update")
123
+ .limit(1);
124
+ if (!enrollment || enrollment.status !== "active") {
125
+ throw new AttachedBrowserInventoryConflictError(
126
+ "attached browser inventory belongs to an unavailable enrollment",
127
+ );
128
+ }
129
+
130
+ const [cursor] = await tx
131
+ .select()
132
+ .from(schema.attachedBrowserInventories)
133
+ .where(
134
+ and(
135
+ eq(schema.attachedBrowserInventories.workspaceId, input.workspaceId),
136
+ eq(schema.attachedBrowserInventories.enrollmentId, input.enrollmentId),
137
+ ),
138
+ )
139
+ .for("update")
140
+ .limit(1);
141
+ if (
142
+ cursor?.bridgeGeneration === snapshot.bridgeGeneration &&
143
+ snapshot.revision < cursor.revision
144
+ ) {
145
+ return {
146
+ accepted: false,
147
+ changed: false,
148
+ revision: await readWorkspaceInteractionRevision(tx, input.workspaceId),
149
+ };
150
+ }
151
+
152
+ const now = new Date();
153
+ const rows = await tx
154
+ .select()
155
+ .from(schema.attachedBrowserDevices)
156
+ .where(eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId))
157
+ .for("update");
158
+ const byId = new Map(rows.map((row) => [row.id, row]));
159
+ let changed = false;
160
+ for (const device of snapshot.devices) {
161
+ const existing = byId.get(device.id);
162
+ if (existing && existing.enrollmentId !== input.enrollmentId) {
163
+ throw new AttachedBrowserInventoryConflictError(
164
+ "attached browser device id is already owned by another enrollment",
165
+ );
166
+ }
167
+ if (!existing) {
168
+ await tx.insert(schema.attachedBrowserDevices).values({
169
+ id: device.id,
170
+ accountId: input.accountId,
171
+ workspaceId: input.workspaceId,
172
+ enrollmentId: input.enrollmentId,
173
+ name: device.name,
174
+ profileLabel: device.profileLabel,
175
+ browserName: device.browserName,
176
+ browserVersion: device.browserVersion,
177
+ extensionVersion: device.extensionVersion,
178
+ platform: device.platform,
179
+ architecture: device.architecture,
180
+ connectionGeneration: device.connectionGeneration,
181
+ inventoryRevision: device.inventoryRevision,
182
+ tabCount: device.tabCount,
183
+ capabilities: device.capabilities,
184
+ lastSeenAt: now,
185
+ disconnectedAt: null,
186
+ createdAt: now,
187
+ updatedAt: now,
188
+ });
189
+ changed = true;
190
+ continue;
191
+ }
192
+ const materialChanged = !sameAnnouncement(existing, device);
193
+ await tx
194
+ .update(schema.attachedBrowserDevices)
195
+ .set({
196
+ name: device.name,
197
+ profileLabel: device.profileLabel,
198
+ browserName: device.browserName,
199
+ browserVersion: device.browserVersion,
200
+ extensionVersion: device.extensionVersion,
201
+ platform: device.platform,
202
+ architecture: device.architecture,
203
+ connectionGeneration: device.connectionGeneration,
204
+ inventoryRevision: device.inventoryRevision,
205
+ tabCount: device.tabCount,
206
+ capabilities: device.capabilities,
207
+ lastSeenAt: now,
208
+ disconnectedAt: null,
209
+ updatedAt: materialChanged ? now : existing.updatedAt,
210
+ })
211
+ .where(
212
+ and(
213
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
214
+ eq(schema.attachedBrowserDevices.id, device.id),
215
+ ),
216
+ );
217
+ changed ||= materialChanged;
218
+ }
219
+
220
+ const announced = new Set(snapshot.devices.map((device) => device.id));
221
+ const disconnectedIds = rows
222
+ .filter(
223
+ (row) =>
224
+ row.enrollmentId === input.enrollmentId &&
225
+ row.disconnectedAt === null &&
226
+ !announced.has(row.id),
227
+ )
228
+ .map((row) => row.id);
229
+ if (disconnectedIds.length > 0) {
230
+ await tx
231
+ .update(schema.attachedBrowserDevices)
232
+ .set({ disconnectedAt: now, updatedAt: now })
233
+ .where(
234
+ and(
235
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
236
+ eq(schema.attachedBrowserDevices.enrollmentId, input.enrollmentId),
237
+ inArray(schema.attachedBrowserDevices.id, disconnectedIds),
238
+ isNull(schema.attachedBrowserDevices.disconnectedAt),
239
+ ),
240
+ );
241
+ changed = true;
242
+ }
243
+
244
+ if (cursor) {
245
+ await tx
246
+ .update(schema.attachedBrowserInventories)
247
+ .set({
248
+ bridgeGeneration: snapshot.bridgeGeneration,
249
+ revision: snapshot.revision,
250
+ observedAt: now,
251
+ updatedAt: now,
252
+ })
253
+ .where(
254
+ and(
255
+ eq(schema.attachedBrowserInventories.workspaceId, input.workspaceId),
256
+ eq(schema.attachedBrowserInventories.enrollmentId, input.enrollmentId),
257
+ ),
258
+ );
259
+ } else {
260
+ await tx.insert(schema.attachedBrowserInventories).values({
261
+ accountId: input.accountId,
262
+ workspaceId: input.workspaceId,
263
+ enrollmentId: input.enrollmentId,
264
+ bridgeGeneration: snapshot.bridgeGeneration,
265
+ revision: snapshot.revision,
266
+ observedAt: now,
267
+ updatedAt: now,
268
+ });
269
+ }
270
+ const revision = changed
271
+ ? await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId)
272
+ : await readWorkspaceInteractionRevision(tx, input.workspaceId);
273
+ return { accepted: true, changed, revision };
274
+ }),
275
+ );
276
+ }
277
+
278
+ /** Immediately mark every live Chrome profile on a cleanly disconnected agent
279
+ * offline. Historical endpoint rows remain discoverable and reconnect in place. */
280
+ export async function disconnectAttachedBrowserDevices(
281
+ db: Database,
282
+ input: { accountId: string; workspaceId: string; enrollmentId: string },
283
+ ): Promise<{ changed: boolean; revision: number }> {
284
+ return await withRlsContext(
285
+ db,
286
+ input,
287
+ async (scopedDb) =>
288
+ await scopedDb.transaction(async (rawTx) => {
289
+ const tx = rawTx as unknown as Database;
290
+ const now = new Date();
291
+ const rows = await tx
292
+ .update(schema.attachedBrowserDevices)
293
+ .set({ disconnectedAt: now, updatedAt: now })
294
+ .where(
295
+ and(
296
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
297
+ eq(schema.attachedBrowserDevices.enrollmentId, input.enrollmentId),
298
+ isNull(schema.attachedBrowserDevices.disconnectedAt),
299
+ ),
300
+ )
301
+ .returning({ id: schema.attachedBrowserDevices.id });
302
+ const revision = rows.length
303
+ ? await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId)
304
+ : await readWorkspaceInteractionRevision(tx, input.workspaceId);
305
+ return { changed: rows.length > 0, revision };
306
+ }),
307
+ );
308
+ }
309
+
310
+ export async function listAttachedBrowserDevices(
311
+ db: Database,
312
+ input: {
313
+ accountId: string;
314
+ workspaceId: string;
315
+ includeDisconnected?: boolean;
316
+ },
317
+ ): Promise<AttachedBrowserDeviceListResponseValue> {
318
+ return await withRlsContext(
319
+ db,
320
+ input,
321
+ async (scopedDb) => {
322
+ const rows = await scopedDb
323
+ .select()
324
+ .from(schema.attachedBrowserDevices)
325
+ .where(
326
+ input.includeDisconnected
327
+ ? eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId)
328
+ : and(
329
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
330
+ isNull(schema.attachedBrowserDevices.disconnectedAt),
331
+ ),
332
+ )
333
+ .orderBy(
334
+ desc(schema.attachedBrowserDevices.disconnectedAt),
335
+ desc(schema.attachedBrowserDevices.updatedAt),
336
+ asc(schema.attachedBrowserDevices.name),
337
+ asc(schema.attachedBrowserDevices.id),
338
+ );
339
+ return AttachedBrowserDeviceListResponse.parse({
340
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
341
+ devices: rows.map(deviceFromRow),
342
+ });
343
+ },
344
+ CONSISTENT_READ,
345
+ );
346
+ }
347
+
348
+ export async function getAttachedBrowserDevice(
349
+ db: Database,
350
+ input: { accountId: string; workspaceId: string; deviceId: string },
351
+ ): Promise<AttachedBrowserDeviceValue> {
352
+ return await withRlsContext(
353
+ db,
354
+ input,
355
+ async (scopedDb) => {
356
+ const [row] = await scopedDb
357
+ .select()
358
+ .from(schema.attachedBrowserDevices)
359
+ .where(
360
+ and(
361
+ eq(schema.attachedBrowserDevices.workspaceId, input.workspaceId),
362
+ eq(schema.attachedBrowserDevices.id, input.deviceId),
363
+ ),
364
+ )
365
+ .limit(1);
366
+ if (!row) throw new AttachedBrowserDeviceNotFoundError("Attached browser not found");
367
+ return deviceFromRow(row);
368
+ },
369
+ CONSISTENT_READ,
370
+ );
371
+ }
@@ -0,0 +1,131 @@
1
+ import { parseVerifiedAttemptToolCatalog } from "@opengeni/codemode";
2
+ import type { AttemptToolCatalog } from "@opengeni/contracts";
3
+ import { and, eq } from "drizzle-orm";
4
+ import type { Database } from "./database";
5
+ import { withRlsContext } from "./database";
6
+ import * as schema from "./schema";
7
+
8
+ export class AttemptToolCatalogAuthorityError extends Error {
9
+ readonly code = "attempt_tool_catalog_authority_mismatch";
10
+
11
+ constructor() {
12
+ super("Attempt tool catalog does not match the exact durable execution attempt");
13
+ this.name = "AttemptToolCatalogAuthorityError";
14
+ }
15
+ }
16
+
17
+ export class AttemptToolCatalogConflictError extends Error {
18
+ readonly code = "attempt_tool_catalog_conflict";
19
+
20
+ constructor() {
21
+ super("Execution attempt already has a different immutable tool catalog");
22
+ this.name = "AttemptToolCatalogConflictError";
23
+ }
24
+ }
25
+
26
+ export async function persistAttemptToolCatalog(
27
+ db: Database,
28
+ input: AttemptToolCatalog,
29
+ ): Promise<AttemptToolCatalog> {
30
+ const catalog = parseVerifiedAttemptToolCatalog(input);
31
+ return await withRlsContext(
32
+ db,
33
+ { accountId: catalog.accountId, workspaceId: catalog.workspaceId },
34
+ async (scopedDb) =>
35
+ await scopedDb.transaction(async (tx) => {
36
+ const [attempt] = await tx
37
+ .select({
38
+ accountId: schema.sessionTurnAttempts.accountId,
39
+ workspaceId: schema.sessionTurnAttempts.workspaceId,
40
+ sessionId: schema.sessionTurnAttempts.sessionId,
41
+ turnId: schema.sessionTurnAttempts.turnId,
42
+ executionGeneration: schema.sessionTurnAttempts.executionGeneration,
43
+ })
44
+ .from(schema.sessionTurnAttempts)
45
+ .where(
46
+ and(
47
+ eq(schema.sessionTurnAttempts.workspaceId, catalog.workspaceId),
48
+ eq(schema.sessionTurnAttempts.id, catalog.attemptId),
49
+ ),
50
+ )
51
+ .limit(1);
52
+ if (
53
+ !attempt ||
54
+ attempt.accountId !== catalog.accountId ||
55
+ attempt.sessionId !== catalog.sessionId ||
56
+ attempt.turnId !== catalog.turnId ||
57
+ attempt.executionGeneration !== catalog.executionGeneration
58
+ ) {
59
+ throw new AttemptToolCatalogAuthorityError();
60
+ }
61
+
62
+ await tx
63
+ .insert(schema.sessionAttemptToolCatalogs)
64
+ .values({
65
+ attemptId: catalog.attemptId,
66
+ accountId: catalog.accountId,
67
+ workspaceId: catalog.workspaceId,
68
+ sessionId: catalog.sessionId,
69
+ turnId: catalog.turnId,
70
+ executionGeneration: catalog.executionGeneration,
71
+ catalogVersion: catalog.version,
72
+ generation: catalog.generation,
73
+ digest: catalog.digest,
74
+ catalog,
75
+ createdAt: new Date(catalog.createdAt),
76
+ })
77
+ .onConflictDoNothing({ target: schema.sessionAttemptToolCatalogs.attemptId });
78
+
79
+ const stored = await attemptToolCatalogByAttemptTx(
80
+ tx,
81
+ catalog.workspaceId,
82
+ catalog.attemptId,
83
+ );
84
+ if (!stored) throw new Error("Failed to persist attempt tool catalog");
85
+ if (stored.digest !== catalog.digest || stored.generation !== catalog.generation) {
86
+ throw new AttemptToolCatalogConflictError();
87
+ }
88
+ return parseVerifiedAttemptToolCatalog(stored.catalog);
89
+ }),
90
+ );
91
+ }
92
+
93
+ export async function getAttemptToolCatalog(
94
+ db: Database,
95
+ input: { accountId: string; workspaceId: string; attemptId: string },
96
+ ): Promise<AttemptToolCatalog | null> {
97
+ return await withRlsContext(
98
+ db,
99
+ { accountId: input.accountId, workspaceId: input.workspaceId },
100
+ async (scopedDb) => {
101
+ const stored = await attemptToolCatalogByAttemptTx(
102
+ scopedDb,
103
+ input.workspaceId,
104
+ input.attemptId,
105
+ );
106
+ return stored ? parseVerifiedAttemptToolCatalog(stored.catalog) : null;
107
+ },
108
+ );
109
+ }
110
+
111
+ async function attemptToolCatalogByAttemptTx(
112
+ tx: Pick<Database, "select">,
113
+ workspaceId: string,
114
+ attemptId: string,
115
+ ) {
116
+ const [row] = await tx
117
+ .select({
118
+ generation: schema.sessionAttemptToolCatalogs.generation,
119
+ digest: schema.sessionAttemptToolCatalogs.digest,
120
+ catalog: schema.sessionAttemptToolCatalogs.catalog,
121
+ })
122
+ .from(schema.sessionAttemptToolCatalogs)
123
+ .where(
124
+ and(
125
+ eq(schema.sessionAttemptToolCatalogs.workspaceId, workspaceId),
126
+ eq(schema.sessionAttemptToolCatalogs.attemptId, attemptId),
127
+ ),
128
+ )
129
+ .limit(1);
130
+ return row ?? null;
131
+ }