@opengeni/db 0.36.1 → 3.1.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 (274) 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-GYO7ED2Q.js → chunk-EULAN2NP.js} +2 -2
  7. package/dist/{chunk-VTPRIXKG.js → chunk-NJVZPWXY.js} +3 -3
  8. package/dist/{chunk-HSOWSYOX.js → chunk-NMUBV7Z5.js} +19 -11
  9. package/dist/chunk-NMUBV7Z5.js.map +1 -0
  10. package/dist/chunk-OF5OBB76.js +4457 -0
  11. package/dist/chunk-OF5OBB76.js.map +1 -0
  12. package/dist/{chunk-5KTIEDX7.js → chunk-PR2WMLOQ.js} +7714 -4443
  13. package/dist/chunk-PR2WMLOQ.js.map +1 -0
  14. package/dist/{chunk-Z7VYMNPZ.js → chunk-SCPUJS53.js} +41 -4
  15. package/dist/chunk-SCPUJS53.js.map +1 -0
  16. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  17. package/dist/chunk-SIBYBA2D.js.map +1 -0
  18. package/dist/chunk-TIKF3KTI.js +383 -0
  19. package/dist/chunk-TIKF3KTI.js.map +1 -0
  20. package/dist/chunk-ZZ5LJEFX.js +516 -0
  21. package/dist/chunk-ZZ5LJEFX.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 +41 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +19 -0
  35. package/dist/editable-artifact-durable-export.d.ts +10 -0
  36. package/dist/editable-artifact-durable-export.js +18 -3
  37. package/dist/editable-artifact-durable-export.js.map +1 -1
  38. package/dist/editable-artifacts.d.ts +2 -2
  39. package/dist/editable-artifacts.js +3 -3
  40. package/dist/governed-learning-activation-schema.d.ts +984 -0
  41. package/dist/governed-learning-activation.d.ts +98 -0
  42. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  43. package/dist/governed-learning-evaluator.d.ts +28 -0
  44. package/dist/index.d.ts +1633 -87
  45. package/dist/index.js +22004 -8109
  46. package/dist/index.js.map +1 -1
  47. package/dist/insights.d.ts +18 -0
  48. package/dist/integration-bindings.d.ts +1 -0
  49. package/dist/integration-facets.d.ts +30 -0
  50. package/dist/interaction-placement-loss.d.ts +22 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +730 -0
  52. package/dist/knowledge-source-sync.d.ts +60 -0
  53. package/dist/managed-human-provisioning.d.ts +25 -0
  54. package/dist/memory-domain.d.ts +3 -3
  55. package/dist/migrate.d.ts +11 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/new-session-drafts.d.ts +43 -5
  58. package/dist/organization-membership-backfill.d.ts +174 -0
  59. package/dist/organization-membership-lifecycle.d.ts +162 -0
  60. package/dist/pr-review.d.ts +107 -0
  61. package/dist/preference-registry-schema.d.ts +2 -0
  62. package/dist/preference-registry.d.ts +2 -0
  63. package/dist/provision-roles.js +1 -1
  64. package/dist/runtime-posture.d.ts +23 -9
  65. package/dist/schema.d.ts +24925 -13697
  66. package/dist/schema.js +103 -1
  67. package/dist/scoped-knowledge-schema.d.ts +52 -1
  68. package/dist/session-background-commands.d.ts +125 -0
  69. package/dist/session-background-commands.js +34 -0
  70. package/dist/session-background-commands.js.map +1 -0
  71. package/dist/session-control.d.ts +156 -3
  72. package/dist/session-goal-pacing.d.ts +68 -0
  73. package/dist/session-queue-commands.d.ts +42 -5
  74. package/dist/session-realtime-ledger.d.ts +3 -0
  75. package/dist/session-tenancy.d.ts +71 -1
  76. package/dist/session-tenancy.js +25 -3
  77. package/dist/session-tool-call-settlement.d.ts +9 -4
  78. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  79. package/dist/slack-routing.d.ts +153 -0
  80. package/dist/slack-task-policy-schema.d.ts +34 -0
  81. package/dist/slack-task-policy.d.ts +8 -0
  82. package/dist/slack-user-link-access.d.ts +1 -0
  83. package/dist/task-notes-schema.d.ts +594 -0
  84. package/dist/task-notes.d.ts +39 -0
  85. package/dist/turn-initiator.d.ts +2 -0
  86. package/dist/user-resource-authority.d.ts +109 -0
  87. package/dist/video-generation.js +3 -3
  88. package/dist/workspace-authority.d.ts +81 -0
  89. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  90. package/dist/workspace-instruction-policies.d.ts +62 -0
  91. package/dist/workspace-learning-policy.d.ts +1 -0
  92. package/dist/workspace-membership-permissions.d.ts +2 -0
  93. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  94. package/drizzle/0240_model_context_user_messages.sql +204 -0
  95. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  96. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  97. package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
  98. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  99. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  100. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  101. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  102. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  103. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  104. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  105. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  106. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  107. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  108. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  109. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  110. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  111. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  112. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  113. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  114. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  115. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  116. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  117. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  118. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  119. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  120. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  121. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  122. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  123. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  124. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  125. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  126. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  127. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  128. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  129. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  130. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  131. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  132. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  133. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  134. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  135. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  136. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  137. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  138. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  139. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  140. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  141. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  142. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  143. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  144. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  145. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  146. package/drizzle/0294_preference_activation_authority.sql +237 -0
  147. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  148. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  149. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  150. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  151. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  152. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  153. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  154. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  155. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  156. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  157. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  158. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  159. package/drizzle/0307_session_attention_state.sql +51 -0
  160. package/drizzle/0308_session_archives.sql +35 -0
  161. package/drizzle/0309_channel_project_pins.sql +6 -0
  162. package/drizzle/0310_channel_project_order.sql +27 -0
  163. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  164. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  165. package/drizzle/0313_private_child_session_authority.sql +365 -0
  166. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  167. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  168. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  169. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  170. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  171. package/drizzle/0319_event_triggered_automations.sql +254 -0
  172. package/drizzle/0320_pr_review_pack.sql +113 -0
  173. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  174. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  175. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  176. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  177. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  178. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  179. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  180. package/drizzle/0328_session_background_commands.sql +590 -0
  181. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  182. package/drizzle/0330_api_key_descriptions.sql +20 -0
  183. package/drizzle/0331_managed_organization_creation.sql +163 -0
  184. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  185. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  186. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  187. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  188. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  189. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  190. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  191. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  192. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  193. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -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 +81 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +75 -2
  214. package/src/editable-artifact-durable-export.ts +27 -0
  215. package/src/editable-artifacts-schema.ts +10 -5
  216. package/src/editable-artifacts.ts +21 -11
  217. package/src/governed-learning-activation-schema.ts +121 -0
  218. package/src/governed-learning-activation.ts +588 -0
  219. package/src/governed-learning-evaluator-schema.ts +72 -0
  220. package/src/governed-learning-evaluator.ts +217 -0
  221. package/src/index.ts +12001 -1699
  222. package/src/insights.ts +107 -0
  223. package/src/integration-bindings.ts +88 -6
  224. package/src/integration-facets.ts +287 -84
  225. package/src/interaction-placement-loss.ts +234 -0
  226. package/src/knowledge-source-sync-schema.ts +83 -0
  227. package/src/knowledge-source-sync.ts +530 -0
  228. package/src/managed-human-provisioning.ts +116 -0
  229. package/src/memory-domain.ts +27 -32
  230. package/src/migrate.ts +77 -1
  231. package/src/new-session-drafts.ts +229 -35
  232. package/src/organization-membership-backfill.ts +735 -0
  233. package/src/organization-membership-lifecycle.ts +985 -0
  234. package/src/pr-review.ts +890 -0
  235. package/src/preference-registry.ts +31 -1
  236. package/src/provision-roles.ts +931 -24
  237. package/src/runtime-posture-cli.ts +2 -0
  238. package/src/runtime-posture.ts +1315 -52
  239. package/src/schema.ts +3336 -438
  240. package/src/scoped-knowledge-schema.ts +16 -8
  241. package/src/scoped-knowledge.ts +1 -1
  242. package/src/session-background-commands.ts +759 -0
  243. package/src/session-control.ts +640 -74
  244. package/src/session-goal-pacing.ts +126 -0
  245. package/src/session-queue-commands.ts +450 -98
  246. package/src/session-realtime-context.ts +19 -3
  247. package/src/session-realtime-ledger.ts +75 -12
  248. package/src/session-realtime-mirror.ts +1 -1
  249. package/src/session-tenancy.ts +403 -49
  250. package/src/session-tool-call-settlement.ts +38 -29
  251. package/src/slack-routing-personal-workspace.ts +67 -0
  252. package/src/slack-routing.ts +469 -0
  253. package/src/slack-task-policy-schema.ts +5 -0
  254. package/src/slack-task-policy.ts +18 -0
  255. package/src/slack-user-link-access.ts +17 -1
  256. package/src/task-notes-schema.ts +74 -0
  257. package/src/task-notes.ts +124 -2
  258. package/src/turn-initiator.ts +8 -0
  259. package/src/user-resource-authority.ts +377 -0
  260. package/src/workspace-authority.ts +160 -0
  261. package/src/workspace-instruction-policies-schema.ts +53 -1
  262. package/src/workspace-instruction-policies.ts +528 -77
  263. package/src/workspace-learning-policy.ts +21 -5
  264. package/src/workspace-membership-permissions.ts +9 -0
  265. package/dist/chunk-5KTIEDX7.js.map +0 -1
  266. package/dist/chunk-FPVAD4CO.js +0 -117
  267. package/dist/chunk-FPVAD4CO.js.map +0 -1
  268. package/dist/chunk-HSOWSYOX.js.map +0 -1
  269. package/dist/chunk-SK7YYHBI.js.map +0 -1
  270. package/dist/chunk-VKLB2ZGI.js +0 -2389
  271. package/dist/chunk-VKLB2ZGI.js.map +0 -1
  272. package/dist/chunk-Z7VYMNPZ.js.map +0 -1
  273. /package/dist/{chunk-GYO7ED2Q.js.map → chunk-EULAN2NP.js.map} +0 -0
  274. /package/dist/{chunk-VTPRIXKG.js.map → chunk-NJVZPWXY.js.map} +0 -0
@@ -0,0 +1,759 @@
1
+ import type {
2
+ SessionBackgroundCommand,
3
+ SessionBackgroundCommandActivity,
4
+ } from "@opengeni/contracts";
5
+ import { and, desc, eq, inArray, sql } from "drizzle-orm";
6
+
7
+ import type { Database } from "./database";
8
+ import { withRlsContext } from "./database";
9
+ import * as schema from "./schema";
10
+
11
+ export type ConnectedMachineBackgroundCommandProof = {
12
+ outcome: "exited" | "lost";
13
+ exitCode: number | null;
14
+ reason: string;
15
+ observedAt: Date;
16
+ };
17
+
18
+ export type ConnectedMachineBackgroundCommandClaim = {
19
+ commandId: string;
20
+ accountId: string;
21
+ workspaceId: string;
22
+ sessionId: string;
23
+ claimId: string;
24
+ state: "running" | "stopping";
25
+ controlWorkspaceId: string;
26
+ enrollmentId: string;
27
+ connectionInstanceId: string;
28
+ opId: string;
29
+ reconcileAttempts: number;
30
+ proof: ConnectedMachineBackgroundCommandProof | null;
31
+ };
32
+
33
+ type ConnectedMachineBackgroundCommandClaimRow = {
34
+ accountId: string;
35
+ workspaceId: string;
36
+ sessionId: string;
37
+ commandId: string;
38
+ claimId: string;
39
+ commandState: string;
40
+ controlWorkspaceId: string;
41
+ enrollmentId: string;
42
+ connectionInstanceId: string;
43
+ opId: string;
44
+ reconcileAttempts: number | string;
45
+ reconcileProofOutcome: string | null;
46
+ reconcileProofExitCode: number | string | null;
47
+ reconcileProofReason: string | null;
48
+ reconcileProofObservedAt: Date | string | null;
49
+ };
50
+
51
+ function commandPreview(value: string): string {
52
+ const normalized = value.replace(/\s+/g, " ").trim();
53
+ return Array.from(normalized).slice(0, 512).join("");
54
+ }
55
+
56
+ function mapCommand(
57
+ row: typeof schema.sessionBackgroundCommands.$inferSelect,
58
+ ): SessionBackgroundCommand {
59
+ return {
60
+ id: row.id,
61
+ workspaceId: row.workspaceId,
62
+ sessionId: row.sessionId,
63
+ provider: row.provider,
64
+ state: row.state,
65
+ commandPreview: row.commandPreview,
66
+ cancelRequestedAt: row.cancelRequestedAt?.toISOString() ?? null,
67
+ exitCode: row.exitCode ?? null,
68
+ settlementReason: row.settlementReason ?? null,
69
+ startedAt: row.startedAt.toISOString(),
70
+ settledAt: row.settledAt?.toISOString() ?? null,
71
+ updatedAt: row.updatedAt.toISOString(),
72
+ };
73
+ }
74
+
75
+ export async function backgroundCommandActivityForSessions(
76
+ db: Database,
77
+ input: { accountId: string; workspaceId: string; sessionIds: string[] },
78
+ ): Promise<Map<string, SessionBackgroundCommandActivity>> {
79
+ if (input.sessionIds.length === 0) return new Map();
80
+ return await withRlsContext(
81
+ db,
82
+ { accountId: input.accountId, workspaceId: input.workspaceId },
83
+ async (scopedDb) => {
84
+ const rows = await scopedDb
85
+ .select({
86
+ sessionId: schema.sessionBackgroundCommands.sessionId,
87
+ count: sql<number>`count(*)::int`,
88
+ stoppingCount: sql<number>`count(*) filter (where ${schema.sessionBackgroundCommands.state} = 'stopping')::int`,
89
+ })
90
+ .from(schema.sessionBackgroundCommands)
91
+ .where(
92
+ and(
93
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
94
+ inArray(schema.sessionBackgroundCommands.sessionId, input.sessionIds),
95
+ inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
96
+ ),
97
+ )
98
+ .groupBy(schema.sessionBackgroundCommands.sessionId);
99
+ return new Map(
100
+ rows.map((row) => [
101
+ row.sessionId,
102
+ {
103
+ state: Number(row.stoppingCount) > 0 ? ("stopping" as const) : ("running" as const),
104
+ count: Number(row.count),
105
+ },
106
+ ]),
107
+ );
108
+ },
109
+ );
110
+ }
111
+
112
+ export async function listSessionBackgroundCommands(
113
+ db: Database,
114
+ input: { accountId: string; workspaceId: string; sessionId: string },
115
+ ): Promise<SessionBackgroundCommand[]> {
116
+ return await withRlsContext(
117
+ db,
118
+ { accountId: input.accountId, workspaceId: input.workspaceId },
119
+ async (scopedDb) => {
120
+ const rows = await scopedDb
121
+ .select()
122
+ .from(schema.sessionBackgroundCommands)
123
+ .where(
124
+ and(
125
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
126
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
127
+ ),
128
+ )
129
+ .orderBy(
130
+ desc(schema.sessionBackgroundCommands.startedAt),
131
+ desc(schema.sessionBackgroundCommands.id),
132
+ )
133
+ .limit(1000);
134
+ return rows.map(mapCommand);
135
+ },
136
+ );
137
+ }
138
+
139
+ /** @internal Caller must already hold the canonical session/control fence. */
140
+ export async function insertManagedSessionBackgroundCommandInTransaction(
141
+ db: Database,
142
+ input: {
143
+ accountId: string;
144
+ workspaceId: string;
145
+ sessionId: string;
146
+ commandId: string;
147
+ retainedProcessId: string;
148
+ command: string;
149
+ },
150
+ ): Promise<SessionBackgroundCommand> {
151
+ const [process] = await db
152
+ .select({
153
+ accountId: schema.sandboxRetainedProcesses.accountId,
154
+ workspaceId: schema.sandboxRetainedProcesses.workspaceId,
155
+ sessionId: schema.sandboxRetainedProcesses.sessionId,
156
+ state: schema.sandboxRetainedProcesses.state,
157
+ })
158
+ .from(schema.sandboxRetainedProcesses)
159
+ .where(eq(schema.sandboxRetainedProcesses.id, input.retainedProcessId))
160
+ .for("update")
161
+ .limit(1);
162
+ if (
163
+ !process ||
164
+ process.accountId !== input.accountId ||
165
+ process.workspaceId !== input.workspaceId ||
166
+ process.sessionId !== input.sessionId ||
167
+ process.state !== "active"
168
+ ) {
169
+ throw new Error("Managed background command requires its exact active retained process");
170
+ }
171
+ const [row] = await db
172
+ .insert(schema.sessionBackgroundCommands)
173
+ .values({
174
+ id: input.commandId,
175
+ accountId: input.accountId,
176
+ workspaceId: input.workspaceId,
177
+ sessionId: input.sessionId,
178
+ provider: "managed",
179
+ state: "running",
180
+ retainedProcessId: input.retainedProcessId,
181
+ commandPreview: commandPreview(input.command),
182
+ })
183
+ .onConflictDoUpdate({
184
+ target: schema.sessionBackgroundCommands.retainedProcessId,
185
+ targetWhere: sql`${schema.sessionBackgroundCommands.retainedProcessId} is not null`,
186
+ set: { updatedAt: new Date() },
187
+ })
188
+ .returning();
189
+ if (!row) throw new Error("Managed background command adoption returned no row");
190
+ if (
191
+ row.accountId !== input.accountId ||
192
+ row.workspaceId !== input.workspaceId ||
193
+ row.sessionId !== input.sessionId ||
194
+ row.provider !== "managed" ||
195
+ row.retainedProcessId !== input.retainedProcessId
196
+ ) {
197
+ throw new Error("Managed background command adoption conflicted with another identity");
198
+ }
199
+ return mapCommand(row);
200
+ }
201
+
202
+ /** @internal Caller must already hold the canonical exact-attempt fence. */
203
+ export async function insertConnectedMachineSessionBackgroundCommandInTransaction(
204
+ db: Database,
205
+ input: {
206
+ accountId: string;
207
+ workspaceId: string;
208
+ sessionId: string;
209
+ commandId: string;
210
+ controlWorkspaceId: string;
211
+ enrollmentId: string;
212
+ connectionInstanceId: string;
213
+ opId: string;
214
+ command: string;
215
+ },
216
+ ): Promise<SessionBackgroundCommand> {
217
+ const [row] = await db
218
+ .insert(schema.sessionBackgroundCommands)
219
+ .values({
220
+ id: input.commandId,
221
+ accountId: input.accountId,
222
+ workspaceId: input.workspaceId,
223
+ sessionId: input.sessionId,
224
+ provider: "connected_machine",
225
+ state: "running",
226
+ controlWorkspaceId: input.controlWorkspaceId,
227
+ enrollmentId: input.enrollmentId,
228
+ connectionInstanceId: input.connectionInstanceId,
229
+ opId: input.opId,
230
+ commandPreview: commandPreview(input.command),
231
+ })
232
+ .onConflictDoUpdate({
233
+ target: [
234
+ schema.sessionBackgroundCommands.controlWorkspaceId,
235
+ schema.sessionBackgroundCommands.enrollmentId,
236
+ schema.sessionBackgroundCommands.connectionInstanceId,
237
+ schema.sessionBackgroundCommands.opId,
238
+ ],
239
+ targetWhere: sql`${schema.sessionBackgroundCommands.provider} = 'connected_machine'`,
240
+ set: { updatedAt: new Date() },
241
+ })
242
+ .returning();
243
+ if (!row) throw new Error("Connected Machine background command adoption returned no row");
244
+ if (
245
+ row.accountId !== input.accountId ||
246
+ row.workspaceId !== input.workspaceId ||
247
+ row.sessionId !== input.sessionId ||
248
+ row.provider !== "connected_machine" ||
249
+ row.controlWorkspaceId !== input.controlWorkspaceId ||
250
+ row.enrollmentId !== input.enrollmentId ||
251
+ row.connectionInstanceId !== input.connectionInstanceId ||
252
+ row.opId !== input.opId
253
+ ) {
254
+ throw new Error(
255
+ "Connected Machine background command adoption conflicted with another identity",
256
+ );
257
+ }
258
+ return mapCommand(row);
259
+ }
260
+
261
+ /** Claim exact Connected Machine locators globally for provider reconciliation.
262
+ * The SECURITY DEFINER SQL function owns the bounded SKIP LOCKED inventory;
263
+ * claim expiry is coordination recovery only and never implies command loss. */
264
+ export async function claimConnectedMachineSessionBackgroundCommands(
265
+ db: Database,
266
+ input: { claimId: string; limit: number; claimTtlMs: number },
267
+ ): Promise<ConnectedMachineBackgroundCommandClaim[]> {
268
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) {
269
+ throw new Error("Connected command reconciliation limit must be between 1 and 100");
270
+ }
271
+ if (
272
+ !Number.isSafeInteger(input.claimTtlMs) ||
273
+ input.claimTtlMs < 0 ||
274
+ input.claimTtlMs > 3_600_000
275
+ ) {
276
+ throw new Error("Connected command reconciliation claim TTL is invalid");
277
+ }
278
+ const rows = await db.execute<ConnectedMachineBackgroundCommandClaimRow>(sql`
279
+ select account_id as "accountId",
280
+ workspace_id as "workspaceId",
281
+ session_id as "sessionId",
282
+ command_id as "commandId",
283
+ claim_id as "claimId",
284
+ command_state as "commandState",
285
+ control_workspace_id as "controlWorkspaceId",
286
+ enrollment_id as "enrollmentId",
287
+ connection_instance_id as "connectionInstanceId",
288
+ op_id as "opId",
289
+ reconcile_attempts as "reconcileAttempts",
290
+ reconcile_proof_outcome as "reconcileProofOutcome",
291
+ reconcile_proof_exit_code as "reconcileProofExitCode",
292
+ reconcile_proof_reason as "reconcileProofReason",
293
+ reconcile_proof_observed_at as "reconcileProofObservedAt"
294
+ from opengeni_private.claim_connected_machine_background_commands(
295
+ ${input.claimId}::uuid, ${input.limit}::integer, ${input.claimTtlMs}::bigint
296
+ )
297
+ `);
298
+ return rows.map((row: ConnectedMachineBackgroundCommandClaimRow) => {
299
+ if (row.commandState !== "running" && row.commandState !== "stopping") {
300
+ throw new Error(`Connected command claim returned invalid state ${row.commandState}`);
301
+ }
302
+ const proof =
303
+ row.reconcileProofOutcome === null
304
+ ? null
305
+ : row.reconcileProofOutcome === "exited" || row.reconcileProofOutcome === "lost"
306
+ ? {
307
+ outcome: row.reconcileProofOutcome,
308
+ exitCode:
309
+ row.reconcileProofExitCode === null ? null : Number(row.reconcileProofExitCode),
310
+ reason: row.reconcileProofReason!,
311
+ observedAt:
312
+ row.reconcileProofObservedAt instanceof Date
313
+ ? row.reconcileProofObservedAt
314
+ : new Date(row.reconcileProofObservedAt!),
315
+ }
316
+ : (() => {
317
+ throw new Error(
318
+ `Connected command claim returned invalid proof ${row.reconcileProofOutcome}`,
319
+ );
320
+ })();
321
+ return {
322
+ commandId: row.commandId,
323
+ accountId: row.accountId,
324
+ workspaceId: row.workspaceId,
325
+ sessionId: row.sessionId,
326
+ claimId: row.claimId,
327
+ state: row.commandState,
328
+ controlWorkspaceId: row.controlWorkspaceId,
329
+ enrollmentId: row.enrollmentId,
330
+ connectionInstanceId: row.connectionInstanceId,
331
+ opId: row.opId,
332
+ reconcileAttempts: Number(row.reconcileAttempts),
333
+ proof,
334
+ };
335
+ });
336
+ }
337
+
338
+ function connectedClaimIdentityWhere(claim: ConnectedMachineBackgroundCommandClaim) {
339
+ return and(
340
+ eq(schema.sessionBackgroundCommands.id, claim.commandId),
341
+ eq(schema.sessionBackgroundCommands.accountId, claim.accountId),
342
+ eq(schema.sessionBackgroundCommands.workspaceId, claim.workspaceId),
343
+ eq(schema.sessionBackgroundCommands.sessionId, claim.sessionId),
344
+ eq(schema.sessionBackgroundCommands.provider, "connected_machine"),
345
+ eq(schema.sessionBackgroundCommands.controlWorkspaceId, claim.controlWorkspaceId),
346
+ eq(schema.sessionBackgroundCommands.enrollmentId, claim.enrollmentId),
347
+ eq(schema.sessionBackgroundCommands.connectionInstanceId, claim.connectionInstanceId),
348
+ eq(schema.sessionBackgroundCommands.opId, claim.opId),
349
+ eq(schema.sessionBackgroundCommands.reconcileClaimId, claim.claimId),
350
+ inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
351
+ );
352
+ }
353
+
354
+ /** Checkpoint one exact terminal provider observation before lifecycle settlement.
355
+ * A retry may reuse an identical proof; a divergent proof is a hard identity fault. */
356
+ export async function recordConnectedMachineBackgroundCommandProof(
357
+ db: Database,
358
+ input: {
359
+ claim: ConnectedMachineBackgroundCommandClaim;
360
+ proof: ConnectedMachineBackgroundCommandProof;
361
+ },
362
+ ): Promise<void> {
363
+ const reason = input.proof.reason.trim().slice(0, 512);
364
+ if (!reason) throw new Error("Connected command proof reason must not be empty");
365
+ if (input.proof.outcome === "exited" && input.proof.exitCode === null) {
366
+ throw new Error("Connected command exit proof requires an exit code");
367
+ }
368
+ if (input.proof.outcome === "lost" && input.proof.exitCode !== null) {
369
+ throw new Error("Connected command loss proof cannot carry an exit code");
370
+ }
371
+ await withRlsContext(
372
+ db,
373
+ { accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
374
+ async (scopedDb) => {
375
+ const [current] = await scopedDb
376
+ .select()
377
+ .from(schema.sessionBackgroundCommands)
378
+ .where(connectedClaimIdentityWhere(input.claim))
379
+ .for("update")
380
+ .limit(1);
381
+ if (!current) throw new Error("Connected command proof was fenced by newer lifecycle state");
382
+ if (current.reconcileProofOutcome !== null) {
383
+ const observedAt = current.reconcileProofObservedAt?.getTime() ?? null;
384
+ if (
385
+ current.reconcileProofOutcome !== input.proof.outcome ||
386
+ current.reconcileProofExitCode !== input.proof.exitCode ||
387
+ current.reconcileProofReason !== reason ||
388
+ observedAt !== input.proof.observedAt.getTime()
389
+ ) {
390
+ throw new Error("Connected command reconciliation proof conflicts with durable proof");
391
+ }
392
+ return;
393
+ }
394
+ const rows = await scopedDb
395
+ .update(schema.sessionBackgroundCommands)
396
+ .set({
397
+ reconcileProofOutcome: input.proof.outcome,
398
+ reconcileProofExitCode: input.proof.exitCode,
399
+ reconcileProofReason: reason,
400
+ reconcileProofObservedAt: input.proof.observedAt,
401
+ lastReconcileOutcome: `proof_${input.proof.outcome}`,
402
+ updatedAt: new Date(),
403
+ })
404
+ .where(connectedClaimIdentityWhere(input.claim))
405
+ .returning({ id: schema.sessionBackgroundCommands.id });
406
+ if (rows.length !== 1) {
407
+ throw new Error("Connected command proof lost its exact claim during checkpoint");
408
+ }
409
+ },
410
+ );
411
+ }
412
+
413
+ export async function deferConnectedMachineBackgroundCommandReconciliation(
414
+ db: Database,
415
+ input: {
416
+ claim: ConnectedMachineBackgroundCommandClaim;
417
+ outcome: string;
418
+ retryAfterMs: number;
419
+ },
420
+ ): Promise<boolean> {
421
+ if (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0) {
422
+ throw new Error("Connected command reconciliation retry delay is invalid");
423
+ }
424
+ const outcome = input.outcome.trim().slice(0, 64);
425
+ if (!outcome) throw new Error("Connected command reconciliation outcome must not be empty");
426
+ return await withRlsContext(
427
+ db,
428
+ { accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
429
+ async (scopedDb) => {
430
+ const rows = await scopedDb
431
+ .update(schema.sessionBackgroundCommands)
432
+ .set({
433
+ reconcileAfter: new Date(Date.now() + input.retryAfterMs),
434
+ reconcileClaimId: null,
435
+ reconcileClaimedAt: null,
436
+ lastReconcileOutcome: outcome,
437
+ updatedAt: new Date(),
438
+ })
439
+ .where(connectedClaimIdentityWhere(input.claim))
440
+ .returning({ id: schema.sessionBackgroundCommands.id });
441
+ return rows.length === 1;
442
+ },
443
+ );
444
+ }
445
+
446
+ /** Settle only from the proof already checkpointed under this exact claim. */
447
+ export async function settleClaimedConnectedMachineBackgroundCommand(
448
+ db: Database,
449
+ input: { claim: ConnectedMachineBackgroundCommandClaim },
450
+ ): Promise<boolean> {
451
+ return await withRlsContext(
452
+ db,
453
+ { accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
454
+ async (scopedDb) => {
455
+ const rows = await scopedDb.execute<{ id: string }>(sql`
456
+ update ${schema.sessionBackgroundCommands} command set
457
+ state = command.reconcile_proof_outcome,
458
+ exit_code = case
459
+ when command.reconcile_proof_outcome = 'exited'
460
+ then command.reconcile_proof_exit_code
461
+ else null
462
+ end,
463
+ settlement_reason = command.reconcile_proof_reason,
464
+ settled_at = command.reconcile_proof_observed_at,
465
+ reconcile_claim_id = null,
466
+ reconcile_claimed_at = null,
467
+ last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
468
+ updated_at = clock_timestamp()
469
+ where command.id = ${input.claim.commandId}
470
+ and command.account_id = ${input.claim.accountId}
471
+ and command.workspace_id = ${input.claim.workspaceId}
472
+ and command.session_id = ${input.claim.sessionId}
473
+ and command.provider = 'connected_machine'
474
+ and command.control_workspace_id = ${input.claim.controlWorkspaceId}
475
+ and command.enrollment_id = ${input.claim.enrollmentId}
476
+ and command.connection_instance_id = ${input.claim.connectionInstanceId}
477
+ and command.op_id = ${input.claim.opId}
478
+ and command.reconcile_claim_id = ${input.claim.claimId}
479
+ and command.state in ('running', 'stopping')
480
+ and command.reconcile_proof_outcome in ('exited', 'lost')
481
+ and command.reconcile_proof_observed_at is not null
482
+ returning command.id
483
+ `);
484
+ return rows.length === 1;
485
+ },
486
+ );
487
+ }
488
+
489
+ export async function requestSessionBackgroundCommandCancellation(
490
+ db: Database,
491
+ input: {
492
+ accountId: string;
493
+ workspaceId: string;
494
+ sessionId: string;
495
+ commandId: string;
496
+ subjectId: string;
497
+ },
498
+ ): Promise<{ command: SessionBackgroundCommand | null; accepted: boolean }> {
499
+ return await withRlsContext(
500
+ db,
501
+ { accountId: input.accountId, workspaceId: input.workspaceId },
502
+ async (scopedDb) =>
503
+ await scopedDb.transaction(async (tx) => {
504
+ const [current] = await tx
505
+ .select()
506
+ .from(schema.sessionBackgroundCommands)
507
+ .where(
508
+ and(
509
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
510
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
511
+ eq(schema.sessionBackgroundCommands.id, input.commandId),
512
+ ),
513
+ )
514
+ .for("update")
515
+ .limit(1);
516
+ if (!current) return { command: null, accepted: false };
517
+ if (current.state !== "running") {
518
+ return { command: mapCommand(current), accepted: false };
519
+ }
520
+ const [updated] = await tx
521
+ .update(schema.sessionBackgroundCommands)
522
+ .set({
523
+ state: "stopping",
524
+ cancelRequestedAt: new Date(),
525
+ cancelRequestedBy: input.subjectId.slice(0, 1024),
526
+ reconcileAfter: new Date(),
527
+ reconcileClaimId: null,
528
+ reconcileClaimedAt: null,
529
+ updatedAt: new Date(),
530
+ })
531
+ .where(eq(schema.sessionBackgroundCommands.id, current.id))
532
+ .returning();
533
+ if (current.retainedProcessId) {
534
+ await tx
535
+ .update(schema.sandboxRetainedProcesses)
536
+ .set({ reconcileAfter: new Date(), lastReconcileOutcome: "cancel_requested" })
537
+ .where(
538
+ and(
539
+ eq(schema.sandboxRetainedProcesses.id, current.retainedProcessId),
540
+ eq(schema.sandboxRetainedProcesses.state, "active"),
541
+ sql`${schema.sandboxRetainedProcesses.reconcileClaimId} is null`,
542
+ ),
543
+ );
544
+ }
545
+ return { command: mapCommand(updated!), accepted: true };
546
+ }),
547
+ );
548
+ }
549
+
550
+ /**
551
+ * Transfer every active command in one or more session subtrees to the
552
+ * provider-neutral stopping state inside the caller's existing control
553
+ * transaction. The workspace-control mutation already excludes concurrent
554
+ * session-tree changes, so the recursive walk and state transition are one
555
+ * atomic Pause/Cancel effect. Steer deliberately never calls this seam.
556
+ */
557
+ export async function requestSessionBackgroundCommandCancellationsInTransaction(
558
+ db: Database,
559
+ input: {
560
+ workspaceId: string;
561
+ rootSessionIds: string[];
562
+ subjectId: string;
563
+ },
564
+ ): Promise<number> {
565
+ const rootSessionIds = [...new Set(input.rootSessionIds)];
566
+ if (rootSessionIds.length === 0) return 0;
567
+ const rows = await db.execute<{ commandCount: number | string }>(sql`
568
+ with recursive roots(id) as (
569
+ values ${sql.join(
570
+ rootSessionIds.map((sessionId) => sql`(${sessionId}::uuid)`),
571
+ sql`, `,
572
+ )}
573
+ ), subtree(id, path, cycle) as (
574
+ select session.id, array[session.id]::uuid[], false
575
+ from roots root
576
+ join ${schema.sessions} session
577
+ on session.workspace_id = ${input.workspaceId}
578
+ and session.id = root.id
579
+ union all
580
+ select child.id, parent.path || child.id, child.id = any(parent.path)
581
+ from subtree parent
582
+ join ${schema.sessions} child
583
+ on child.workspace_id = ${input.workspaceId}
584
+ and child.parent_session_id = parent.id
585
+ where not parent.cycle
586
+ ), stopped as (
587
+ update ${schema.sessionBackgroundCommands} command set
588
+ state = 'stopping',
589
+ cancel_requested_at = clock_timestamp(),
590
+ cancel_requested_by = left(${input.subjectId}, 1024),
591
+ reconcile_after = clock_timestamp(),
592
+ reconcile_claim_id = null,
593
+ reconcile_claimed_at = null,
594
+ updated_at = clock_timestamp()
595
+ where command.workspace_id = ${input.workspaceId}
596
+ and command.session_id in (select id from subtree where not cycle)
597
+ and command.state = 'running'
598
+ returning command.retained_process_id
599
+ ), nudged as (
600
+ update ${schema.sandboxRetainedProcesses} process set
601
+ reconcile_after = clock_timestamp(),
602
+ reconcile_claim_id = null,
603
+ reconcile_claimed_at = null,
604
+ last_reconcile_outcome = 'cancel_requested'
605
+ where process.id in (
606
+ select stopped.retained_process_id
607
+ from stopped
608
+ where stopped.retained_process_id is not null
609
+ )
610
+ and process.state = 'active'
611
+ returning process.id
612
+ )
613
+ select count(*)::integer as "commandCount" from stopped
614
+ `);
615
+ return Number(rows[0]?.commandCount ?? 0);
616
+ }
617
+
618
+ /** Workspace Pause owns every active command in that workspace. */
619
+ export async function requestWorkspaceBackgroundCommandCancellationsInTransaction(
620
+ db: Database,
621
+ input: { workspaceId: string; subjectId: string },
622
+ ): Promise<number> {
623
+ const rows = await db.execute<{ commandCount: number | string }>(sql`
624
+ with stopped as (
625
+ update ${schema.sessionBackgroundCommands} command set
626
+ state = 'stopping',
627
+ cancel_requested_at = clock_timestamp(),
628
+ cancel_requested_by = left(${input.subjectId}, 1024),
629
+ reconcile_after = clock_timestamp(),
630
+ reconcile_claim_id = null,
631
+ reconcile_claimed_at = null,
632
+ updated_at = clock_timestamp()
633
+ where command.workspace_id = ${input.workspaceId}
634
+ and command.state = 'running'
635
+ returning command.retained_process_id
636
+ ), nudged as (
637
+ update ${schema.sandboxRetainedProcesses} process set
638
+ reconcile_after = clock_timestamp(),
639
+ reconcile_claim_id = null,
640
+ reconcile_claimed_at = null,
641
+ last_reconcile_outcome = 'cancel_requested'
642
+ where process.id in (
643
+ select stopped.retained_process_id
644
+ from stopped
645
+ where stopped.retained_process_id is not null
646
+ )
647
+ and process.state = 'active'
648
+ returning process.id
649
+ )
650
+ select count(*)::integer as "commandCount" from stopped
651
+ `);
652
+ return Number(rows[0]?.commandCount ?? 0);
653
+ }
654
+
655
+ export async function settleSessionBackgroundCommandForRetainedProcess(
656
+ db: Database,
657
+ input: {
658
+ accountId: string;
659
+ workspaceId: string;
660
+ sessionId: string;
661
+ retainedProcessId: string;
662
+ outcome: "exited" | "lost";
663
+ exitCode: number | null;
664
+ reason: string;
665
+ },
666
+ ): Promise<SessionBackgroundCommand | null> {
667
+ return await withRlsContext(
668
+ db,
669
+ { accountId: input.accountId, workspaceId: input.workspaceId },
670
+ async (scopedDb) => {
671
+ const [row] = await scopedDb
672
+ .update(schema.sessionBackgroundCommands)
673
+ .set({
674
+ state: input.outcome,
675
+ exitCode: input.outcome === "exited" ? input.exitCode : null,
676
+ settlementReason: input.reason.slice(0, 512),
677
+ settledAt: new Date(),
678
+ reconcileClaimId: null,
679
+ reconcileClaimedAt: null,
680
+ lastReconcileOutcome: `settled_${input.outcome}`,
681
+ updatedAt: new Date(),
682
+ })
683
+ .where(
684
+ and(
685
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
686
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
687
+ eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
688
+ inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
689
+ ),
690
+ )
691
+ .returning();
692
+ return row ? mapCommand(row) : null;
693
+ },
694
+ );
695
+ }
696
+
697
+ export async function settleConnectedMachineSessionBackgroundCommand(
698
+ db: Database,
699
+ input: {
700
+ accountId: string;
701
+ workspaceId: string;
702
+ sessionId: string;
703
+ commandId: string;
704
+ controlWorkspaceId: string;
705
+ enrollmentId: string;
706
+ connectionInstanceId: string;
707
+ opId: string;
708
+ outcome: "exited" | "lost";
709
+ exitCode: number | null;
710
+ reason: string;
711
+ },
712
+ ): Promise<SessionBackgroundCommand | null> {
713
+ return await withRlsContext(
714
+ db,
715
+ { accountId: input.accountId, workspaceId: input.workspaceId },
716
+ async (scopedDb) => {
717
+ const settledAt = new Date();
718
+ const reason = input.reason.trim().slice(0, 512);
719
+ if (!reason) throw new Error("Connected command settlement reason must not be empty");
720
+ if (input.outcome === "exited" && input.exitCode === null) {
721
+ throw new Error("Connected command exit settlement requires an exit code");
722
+ }
723
+ if (input.outcome === "lost" && input.exitCode !== null) {
724
+ throw new Error("Connected command loss settlement cannot carry an exit code");
725
+ }
726
+ const [row] = await scopedDb
727
+ .update(schema.sessionBackgroundCommands)
728
+ .set({
729
+ state: input.outcome,
730
+ exitCode: input.outcome === "exited" ? input.exitCode : null,
731
+ settlementReason: reason,
732
+ settledAt,
733
+ reconcileClaimId: null,
734
+ reconcileClaimedAt: null,
735
+ reconcileProofOutcome: input.outcome,
736
+ reconcileProofExitCode: input.outcome === "exited" ? input.exitCode : null,
737
+ reconcileProofReason: reason,
738
+ reconcileProofObservedAt: settledAt,
739
+ lastReconcileOutcome: `settled_${input.outcome}`,
740
+ updatedAt: new Date(),
741
+ })
742
+ .where(
743
+ and(
744
+ eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
745
+ eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
746
+ eq(schema.sessionBackgroundCommands.id, input.commandId),
747
+ eq(schema.sessionBackgroundCommands.provider, "connected_machine"),
748
+ eq(schema.sessionBackgroundCommands.controlWorkspaceId, input.controlWorkspaceId),
749
+ eq(schema.sessionBackgroundCommands.enrollmentId, input.enrollmentId),
750
+ eq(schema.sessionBackgroundCommands.connectionInstanceId, input.connectionInstanceId),
751
+ eq(schema.sessionBackgroundCommands.opId, input.opId),
752
+ inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
753
+ ),
754
+ )
755
+ .returning();
756
+ return row ? mapCommand(row) : null;
757
+ },
758
+ );
759
+ }