@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,1174 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0278: workspace-membership removal becomes one fenced
3
+ -- SECURITY DEFINER teardown instead of a bare personal-row cleanup + DELETE.
4
+ --
5
+ -- Before this migration `removeWorkspaceMember` deleted the target's
6
+ -- session-list snapshots, pins, drafts and the membership row and fenced
7
+ -- nothing: the removed member's queued/live turns kept running, their
8
+ -- user-private sessions in the workspace kept their authority epoch, their
9
+ -- active realtime modes stayed open, and nothing woke the affected workflows.
10
+ -- Organization offboarding (migration 0263) already performs the canonical
11
+ -- teardown account-wide; this migration is the same teardown scoped to
12
+ -- exactly one workspace and one target subject.
13
+ --
14
+ -- Rolling window. Two new functions only; no table shape changes. An old API
15
+ -- image keeps calling the bare delete path until it is replaced - removal
16
+ -- through the old path stays exactly as weak as before, never weaker, and the
17
+ -- new path composes with every existing invariant:
18
+ -- * the canonical event-write lock order is preserved (control FOR SHARE ->
19
+ -- workspace FOR KEY SHARE -> sessions FOR NO KEY UPDATE -> turns
20
+ -- FOR UPDATE -> attempts FOR UPDATE);
21
+ -- * a turn with pending tool calls and no live attempt must be settled
22
+ -- through the canonical application protocol first - the command fails
23
+ -- closed (55000) exactly like the 0263 command, and the application runs
24
+ -- the same prepare/settle pass before invoking it;
25
+ -- * live attempts are interrupted, never presumed quiesced: the command
26
+ -- records `session_attempt_interruptions` (kind `authority_change`) and
27
+ -- registers workflow wakes; Temporal cancellation semantics are untouched;
28
+ -- * removal never touches other workspaces, other subjects, organization
29
+ -- membership status, personal-workspace pointers, or retention.
30
+ --
31
+ -- Guards, restated from the application layer as fail-closed authority:
32
+ -- * the actor cannot remove their own membership;
33
+ -- * the last member holding an administering permission
34
+ -- ('workspace:admin' or 'members:manage') cannot be removed;
35
+ -- * the actor must either hold an administering permission in this
36
+ -- workspace or be an active organization owner/admin.
37
+ --
38
+ -- Idempotency: the whole command is one transaction. A retry after success
39
+ -- finds no membership row and returns removed=false without re-tearing
40
+ -- anything down; there is no partial state to repair.
41
+
42
+ SET LOCAL lock_timeout = '5s';
43
+ SET LOCAL statement_timeout = '5min';
44
+
45
+ -- 1. Preparation: the same "settle pending tool calls first" contract as the
46
+ -- organization lifecycle, scoped to one workspace + one subject. Returns the
47
+ -- exact turns the application must settle through its canonical protocol
48
+ -- before calling the removal command.
49
+
50
+ CREATE FUNCTION prepare_workspace_membership_removal_settlements(
51
+ p_command jsonb
52
+ ) RETURNS jsonb
53
+ LANGUAGE plpgsql
54
+ SECURITY DEFINER
55
+ SET search_path FROM CURRENT
56
+ AS $body$
57
+ DECLARE
58
+ account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
59
+ workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
60
+ actor_subject text := p_command ->> 'actorSubjectId';
61
+ target_subject text := p_command ->> 'targetSubjectId';
62
+ target_membership_id uuid;
63
+ result jsonb := '[]'::jsonb;
64
+ previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
65
+ BEGIN
66
+ IF p_command IS NULL
67
+ OR (p_command ->> 'action') IS DISTINCT FROM 'remove'
68
+ OR account_id_value IS NULL
69
+ OR workspace_id_value IS NULL
70
+ OR actor_subject IS NULL
71
+ OR target_subject IS NULL
72
+ OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
73
+ OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
74
+ THEN
75
+ RAISE EXCEPTION 'workspace membership removal preparation authority is invalid'
76
+ USING ERRCODE = '42501';
77
+ END IF;
78
+ PERFORM 1 FROM workspaces workspace
79
+ WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
80
+ IF NOT FOUND THEN
81
+ RAISE EXCEPTION 'workspace not found' USING ERRCODE = 'P0002';
82
+ END IF;
83
+ PERFORM opengeni_private.assert_workspace_membership_removal_actor(
84
+ account_id_value, workspace_id_value, actor_subject, target_subject
85
+ );
86
+ SELECT membership.id INTO target_membership_id
87
+ FROM organization_memberships membership
88
+ WHERE membership.account_id = account_id_value
89
+ AND membership.subject_id = target_subject;
90
+
91
+ PERFORM pg_catalog.set_config(
92
+ 'opengeni.workspace_id', workspace_id_value::text, true
93
+ );
94
+ PERFORM 1 FROM workspace_inference_controls control
95
+ WHERE control.account_id = account_id_value
96
+ AND control.workspace_id = workspace_id_value
97
+ FOR SHARE;
98
+ PERFORM 1 FROM workspaces workspace
99
+ WHERE workspace.account_id = account_id_value
100
+ AND workspace.id = workspace_id_value
101
+ FOR KEY SHARE;
102
+ -- Scheduled-task rows before session rows, exactly the 0275 wrapper's
103
+ -- per-workspace task/session prefix: the scheduled-task writers lock
104
+ -- task -> session -> membership, so removal must never hold the membership
105
+ -- (or session) rows while waiting on a task row.
106
+ PERFORM 1 FROM scheduled_tasks task
107
+ WHERE task.account_id = account_id_value
108
+ AND task.workspace_id = workspace_id_value
109
+ AND task.deleted_at IS NULL
110
+ AND task.status = 'active'
111
+ AND (
112
+ (task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
113
+ OR EXISTS (
114
+ SELECT 1 FROM scheduled_task_revision_authorities authority
115
+ WHERE authority.task_id = task.id
116
+ AND authority.task_authority_revision = task.authority_revision
117
+ AND authority.subject_id = target_subject
118
+ )
119
+ OR EXISTS (
120
+ SELECT 1 FROM scheduled_task_personal_resource_authorities authority
121
+ WHERE authority.task_id = task.id
122
+ AND authority.task_authority_revision = task.authority_revision
123
+ AND authority.initiating_human_subject_id = target_subject
124
+ )
125
+ OR EXISTS (
126
+ SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
127
+ WHERE snapshot.task_id = task.id
128
+ AND snapshot.task_authority_revision = task.authority_revision
129
+ AND snapshot.owner_subject_id = target_subject
130
+ )
131
+ )
132
+ ORDER BY task.id
133
+ FOR UPDATE;
134
+ PERFORM 1 FROM sessions affected
135
+ WHERE affected.account_id = account_id_value
136
+ AND affected.workspace_id = workspace_id_value
137
+ AND (
138
+ (target_membership_id IS NOT NULL
139
+ AND affected.owner_organization_membership_id = target_membership_id)
140
+ OR EXISTS (
141
+ SELECT 1 FROM session_turns initiated
142
+ WHERE initiated.session_id = affected.id
143
+ AND initiated.initiating_human_subject_id = target_subject
144
+ )
145
+ )
146
+ ORDER BY affected.id
147
+ FOR NO KEY UPDATE;
148
+ PERFORM 1 FROM session_turns turn_row
149
+ WHERE turn_row.account_id = account_id_value
150
+ AND turn_row.workspace_id = workspace_id_value
151
+ AND EXISTS (
152
+ SELECT 1 FROM sessions owned
153
+ WHERE owned.id = turn_row.session_id
154
+ AND (
155
+ (target_membership_id IS NOT NULL
156
+ AND owned.owner_organization_membership_id = target_membership_id)
157
+ OR turn_row.initiating_human_subject_id = target_subject
158
+ )
159
+ )
160
+ ORDER BY turn_row.id
161
+ FOR UPDATE;
162
+ PERFORM 1 FROM session_turn_attempts attempt
163
+ WHERE attempt.account_id = account_id_value
164
+ AND attempt.workspace_id = workspace_id_value
165
+ AND EXISTS (
166
+ SELECT 1 FROM sessions owned
167
+ WHERE owned.id = attempt.session_id
168
+ AND (
169
+ (target_membership_id IS NOT NULL
170
+ AND owned.owner_organization_membership_id = target_membership_id)
171
+ OR EXISTS (
172
+ SELECT 1 FROM session_turns initiated
173
+ WHERE initiated.id = attempt.turn_id
174
+ AND initiated.initiating_human_subject_id = target_subject
175
+ )
176
+ )
177
+ )
178
+ ORDER BY attempt.id
179
+ FOR UPDATE;
180
+ SELECT result || COALESCE(
181
+ pg_catalog.jsonb_agg(
182
+ pg_catalog.jsonb_build_object(
183
+ 'accountId', turn_row.account_id,
184
+ 'workspaceId', turn_row.workspace_id,
185
+ 'sessionId', turn_row.session_id,
186
+ 'turnId', turn_row.id,
187
+ 'executionGeneration', turn_row.execution_generation,
188
+ 'turnAssociation', CASE WHEN owned.active_turn_id = turn_row.id
189
+ THEN 'current' ELSE NULL END
190
+ ) ORDER BY turn_row.session_id, turn_row.id
191
+ ), '[]'::jsonb
192
+ ) INTO result
193
+ FROM session_turns turn_row
194
+ JOIN sessions owned ON owned.id = turn_row.session_id
195
+ WHERE turn_row.account_id = account_id_value
196
+ AND turn_row.workspace_id = workspace_id_value
197
+ AND turn_row.status IN (
198
+ 'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
199
+ )
200
+ AND (
201
+ (target_membership_id IS NOT NULL
202
+ AND owned.owner_organization_membership_id = target_membership_id)
203
+ OR turn_row.initiating_human_subject_id = target_subject
204
+ )
205
+ AND EXISTS (
206
+ SELECT 1 FROM session_pending_tool_calls pending
207
+ WHERE pending.account_id = turn_row.account_id
208
+ AND pending.workspace_id = turn_row.workspace_id
209
+ AND pending.session_id = turn_row.session_id
210
+ AND pending.turn_id = turn_row.id
211
+ )
212
+ AND NOT EXISTS (
213
+ SELECT 1 FROM session_turn_attempts live_attempt
214
+ WHERE live_attempt.account_id = turn_row.account_id
215
+ AND live_attempt.workspace_id = turn_row.workspace_id
216
+ AND live_attempt.session_id = turn_row.session_id
217
+ AND live_attempt.turn_id = turn_row.id
218
+ AND live_attempt.id = turn_row.active_attempt_id
219
+ AND live_attempt.state IN ('claimed', 'running')
220
+ );
221
+ PERFORM pg_catalog.set_config(
222
+ 'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
223
+ );
224
+ RETURN result;
225
+ EXCEPTION WHEN OTHERS THEN
226
+ PERFORM pg_catalog.set_config(
227
+ 'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
228
+ );
229
+ RAISE;
230
+ END
231
+ $body$;
232
+
233
+ -- 2. Shared actor/guard authority. The exact application guards, restated as
234
+ -- fail-closed database authority so no future caller can skip them:
235
+ -- self-removal, last-admin, and actor administration. The checks themselves
236
+ -- are pure reads: the removal command serializes the administering roster
237
+ -- (ordered FOR UPDATE on every admin/actor/target membership row plus the
238
+ -- actor's organization membership) BEFORE re-running them, so two concurrent
239
+ -- removals of the two last administering members cannot both pass. The
240
+ -- preparation function's earlier call is a friendly unserialized precheck
241
+ -- only and authorizes nothing by itself.
242
+
243
+ CREATE FUNCTION opengeni_private.assert_workspace_membership_removal_actor(
244
+ p_account_id uuid,
245
+ p_workspace_id uuid,
246
+ p_actor_subject text,
247
+ p_target_subject text
248
+ ) RETURNS void
249
+ LANGUAGE plpgsql
250
+ AS $body$
251
+ DECLARE
252
+ actor_can_administer boolean;
253
+ BEGIN
254
+ IF p_actor_subject = p_target_subject THEN
255
+ RAISE EXCEPTION 'a member cannot remove their own workspace membership'
256
+ USING ERRCODE = '55000';
257
+ END IF;
258
+ SELECT EXISTS (
259
+ SELECT 1 FROM workspace_memberships actor_row
260
+ WHERE actor_row.account_id = p_account_id
261
+ AND actor_row.workspace_id = p_workspace_id
262
+ AND actor_row.subject_id = p_actor_subject
263
+ AND actor_row.permissions ?| ARRAY['workspace:admin', 'members:manage']
264
+ ) OR EXISTS (
265
+ SELECT 1 FROM organization_memberships org_actor
266
+ WHERE org_actor.account_id = p_account_id
267
+ AND org_actor.subject_id = p_actor_subject
268
+ AND org_actor.status = 'active'
269
+ AND org_actor.role IN ('owner', 'admin')
270
+ ) INTO actor_can_administer;
271
+ IF NOT actor_can_administer THEN
272
+ RAISE EXCEPTION 'workspace member administration required' USING ERRCODE = '42501';
273
+ END IF;
274
+ IF EXISTS (
275
+ SELECT 1 FROM workspace_memberships target_row
276
+ WHERE target_row.account_id = p_account_id
277
+ AND target_row.workspace_id = p_workspace_id
278
+ AND target_row.subject_id = p_target_subject
279
+ AND target_row.permissions ?| ARRAY['workspace:admin', 'members:manage']
280
+ ) AND NOT EXISTS (
281
+ SELECT 1 FROM workspace_memberships other
282
+ WHERE other.account_id = p_account_id
283
+ AND other.workspace_id = p_workspace_id
284
+ AND other.subject_id <> p_target_subject
285
+ AND other.permissions ?| ARRAY['workspace:admin', 'members:manage']
286
+ ) THEN
287
+ RAISE EXCEPTION 'cannot remove the last administering workspace member'
288
+ USING ERRCODE = '55000';
289
+ END IF;
290
+ END
291
+ $body$;
292
+
293
+ -- 3. The removal command itself.
294
+
295
+ CREATE FUNCTION workspace_membership_removal_command(
296
+ p_command jsonb
297
+ ) RETURNS jsonb
298
+ LANGUAGE plpgsql
299
+ SECURITY DEFINER
300
+ SET search_path FROM CURRENT
301
+ AS $body$
302
+ DECLARE
303
+ account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
304
+ workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
305
+ actor_subject text := p_command ->> 'actorSubjectId';
306
+ target_subject text := p_command ->> 'targetSubjectId';
307
+ operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
308
+ input_hash_value text;
309
+ target_membership_id uuid;
310
+ membership_row_id uuid;
311
+ now_value timestamptz := pg_catalog.clock_timestamp();
312
+ affected_session_ids uuid[] := ARRAY[]::uuid[];
313
+ affected_realtime_ids uuid[] := ARRAY[]::uuid[];
314
+ cancelled_turn_ids uuid[] := ARRAY[]::uuid[];
315
+ cancelled_turn record;
316
+ cancelled_request record;
317
+ cancelled_sequence bigint;
318
+ cancelled_association text;
319
+ settled_update_ids uuid[];
320
+ settled_history_item_id uuid;
321
+ interruption_operation_id uuid;
322
+ visibility_capability_id uuid;
323
+ activity_revision_value bigint;
324
+ previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
325
+ previous_visibility_capability text := pg_catalog.current_setting(
326
+ 'opengeni.session_visibility_write_capability', true
327
+ );
328
+ previous_activity_gate_state text := pg_catalog.current_setting(
329
+ 'opengeni.session_activity_gate_state', true
330
+ );
331
+ previous_activity_gate_workspace text := pg_catalog.current_setting(
332
+ 'opengeni.session_activity_gate_workspace_id', true
333
+ );
334
+ BEGIN
335
+ IF p_command IS NULL
336
+ OR (p_command ->> 'action') IS DISTINCT FROM 'remove'
337
+ OR account_id_value IS NULL
338
+ OR workspace_id_value IS NULL
339
+ OR actor_subject IS NULL
340
+ OR target_subject IS NULL
341
+ OR operation_id_value IS NULL
342
+ OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
343
+ OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
344
+ THEN
345
+ RAISE EXCEPTION 'workspace membership removal authority is invalid'
346
+ USING ERRCODE = '42501';
347
+ END IF;
348
+ input_hash_value := pg_catalog.encode(
349
+ pg_catalog.sha256(pg_catalog.convert_to(p_command::text, 'UTF8')), 'hex'
350
+ );
351
+ PERFORM 1 FROM workspaces workspace
352
+ WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
353
+ IF NOT FOUND THEN
354
+ RAISE EXCEPTION 'workspace not found' USING ERRCODE = 'P0002';
355
+ END IF;
356
+
357
+ -- The command runs after the application settled the target's pending
358
+ -- protocol state and finalized that workspace activity gate. Its own gated
359
+ -- session writes need the commit guards DEFERRED again (exactly the 0275
360
+ -- wrapper's reasoning).
361
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
362
+ sessions_activity_update_commit_guard DEFERRED;
363
+
364
+ -- The personal-state fence the pre-0278 delete path took: session listing
365
+ -- takes its shared counterpart and pin mutation the same exclusive one, so
366
+ -- a racing snapshot/pin writer cannot recreate personal rows after this
367
+ -- cleanup. Same key derivation as lockSessionPersonalStateExclusive.
368
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
369
+ 'session-personal-state:' || workspace_id_value::text || ':' || target_subject, 0
370
+ ));
371
+
372
+ visibility_capability_id := pg_catalog.gen_random_uuid();
373
+ INSERT INTO session_visibility_write_capabilities (
374
+ backend_pid, transaction_id, capability_id
375
+ ) VALUES (
376
+ pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(),
377
+ visibility_capability_id
378
+ );
379
+ PERFORM pg_catalog.set_config(
380
+ 'opengeni.session_visibility_write_capability',
381
+ visibility_capability_id::text, true
382
+ );
383
+ PERFORM pg_catalog.set_config(
384
+ 'opengeni.workspace_id', workspace_id_value::text, true
385
+ );
386
+ PERFORM pg_catalog.set_config(
387
+ 'opengeni.session_activity_gate_state', 'open', true
388
+ );
389
+ PERFORM pg_catalog.set_config(
390
+ 'opengeni.session_activity_gate_workspace_id', workspace_id_value::text, true
391
+ );
392
+
393
+ -- Canonical lock prefix for this workspace, then the exact affected rows.
394
+ PERFORM 1 FROM workspace_inference_controls control
395
+ WHERE control.account_id = account_id_value
396
+ AND control.workspace_id = workspace_id_value
397
+ FOR SHARE;
398
+ PERFORM 1 FROM workspaces workspace
399
+ WHERE workspace.account_id = account_id_value
400
+ AND workspace.id = workspace_id_value
401
+ FOR KEY SHARE;
402
+ -- Task rows before membership/session rows (0275 writer order; reentrant
403
+ -- with the preparation pass in the same transaction).
404
+ PERFORM 1 FROM scheduled_tasks task
405
+ WHERE task.account_id = account_id_value
406
+ AND task.workspace_id = workspace_id_value
407
+ AND task.deleted_at IS NULL
408
+ AND task.status = 'active'
409
+ AND (
410
+ (task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
411
+ OR EXISTS (
412
+ SELECT 1 FROM scheduled_task_revision_authorities authority
413
+ WHERE authority.task_id = task.id
414
+ AND authority.task_authority_revision = task.authority_revision
415
+ AND authority.subject_id = target_subject
416
+ )
417
+ OR EXISTS (
418
+ SELECT 1 FROM scheduled_task_personal_resource_authorities authority
419
+ WHERE authority.task_id = task.id
420
+ AND authority.task_authority_revision = task.authority_revision
421
+ AND authority.initiating_human_subject_id = target_subject
422
+ )
423
+ OR EXISTS (
424
+ SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
425
+ WHERE snapshot.task_id = task.id
426
+ AND snapshot.task_authority_revision = task.authority_revision
427
+ AND snapshot.owner_subject_id = target_subject
428
+ )
429
+ )
430
+ ORDER BY task.id
431
+ FOR UPDATE;
432
+ -- Serialize the administering roster before re-running the guards: the
433
+ -- actor's organization membership (shared - the organization lifecycle
434
+ -- takes it FOR UPDATE) and every admin/actor/target workspace membership
435
+ -- row in id order. Two concurrent removals of the two last administering
436
+ -- members therefore serialize, and the loser re-reads a roster with only
437
+ -- one administering member left and fails closed.
438
+ PERFORM 1 FROM organization_memberships org_actor
439
+ WHERE org_actor.account_id = account_id_value
440
+ AND org_actor.subject_id = actor_subject
441
+ FOR SHARE;
442
+ PERFORM 1 FROM workspace_memberships roster
443
+ WHERE roster.account_id = account_id_value
444
+ AND roster.workspace_id = workspace_id_value
445
+ AND (
446
+ roster.subject_id IN (actor_subject, target_subject)
447
+ OR roster.permissions ?| ARRAY['workspace:admin', 'members:manage']
448
+ )
449
+ ORDER BY roster.id
450
+ FOR UPDATE;
451
+ PERFORM opengeni_private.assert_workspace_membership_removal_actor(
452
+ account_id_value, workspace_id_value, actor_subject, target_subject
453
+ );
454
+ SELECT membership.id INTO membership_row_id
455
+ FROM workspace_memberships membership
456
+ WHERE membership.account_id = account_id_value
457
+ AND membership.workspace_id = workspace_id_value
458
+ AND membership.subject_id = target_subject;
459
+ IF membership_row_id IS NULL THEN
460
+ DELETE FROM session_visibility_write_capabilities capability
461
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
462
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id()
463
+ AND capability.capability_id = visibility_capability_id;
464
+ PERFORM pg_catalog.set_config(
465
+ 'opengeni.session_visibility_write_capability',
466
+ coalesce(previous_visibility_capability, ''), true
467
+ );
468
+ PERFORM pg_catalog.set_config(
469
+ 'opengeni.workspace_id', coalesce(previous_workspace, ''), true
470
+ );
471
+ PERFORM pg_catalog.set_config(
472
+ 'opengeni.session_activity_gate_state',
473
+ coalesce(previous_activity_gate_state, ''), true
474
+ );
475
+ PERFORM pg_catalog.set_config(
476
+ 'opengeni.session_activity_gate_workspace_id',
477
+ coalesce(previous_activity_gate_workspace, ''), true
478
+ );
479
+ RETURN pg_catalog.jsonb_build_object('removed', false);
480
+ END IF;
481
+ SELECT membership.id INTO target_membership_id
482
+ FROM organization_memberships membership
483
+ WHERE membership.account_id = account_id_value
484
+ AND membership.subject_id = target_subject;
485
+ PERFORM 1 FROM sessions affected
486
+ WHERE affected.account_id = account_id_value
487
+ AND affected.workspace_id = workspace_id_value
488
+ AND (
489
+ (target_membership_id IS NOT NULL
490
+ AND affected.owner_organization_membership_id = target_membership_id)
491
+ OR EXISTS (
492
+ SELECT 1 FROM session_turns initiated
493
+ WHERE initiated.session_id = affected.id
494
+ AND initiated.initiating_human_subject_id = target_subject
495
+ )
496
+ )
497
+ ORDER BY affected.id
498
+ FOR NO KEY UPDATE;
499
+ PERFORM 1 FROM session_turns turn_row
500
+ WHERE turn_row.account_id = account_id_value
501
+ AND turn_row.workspace_id = workspace_id_value
502
+ AND EXISTS (
503
+ SELECT 1 FROM sessions owned
504
+ WHERE owned.id = turn_row.session_id
505
+ AND (
506
+ (target_membership_id IS NOT NULL
507
+ AND owned.owner_organization_membership_id = target_membership_id)
508
+ OR turn_row.initiating_human_subject_id = target_subject
509
+ )
510
+ )
511
+ ORDER BY turn_row.id
512
+ FOR UPDATE;
513
+ PERFORM 1 FROM session_turn_attempts attempt
514
+ WHERE attempt.account_id = account_id_value
515
+ AND attempt.workspace_id = workspace_id_value
516
+ AND EXISTS (
517
+ SELECT 1 FROM sessions owned
518
+ WHERE owned.id = attempt.session_id
519
+ AND (
520
+ (target_membership_id IS NOT NULL
521
+ AND owned.owner_organization_membership_id = target_membership_id)
522
+ OR EXISTS (
523
+ SELECT 1 FROM session_turns initiated
524
+ WHERE initiated.id = attempt.turn_id
525
+ AND initiated.initiating_human_subject_id = target_subject
526
+ )
527
+ )
528
+ )
529
+ ORDER BY attempt.id
530
+ FOR UPDATE;
531
+ PERFORM 1 FROM session_realtime_modes realtime
532
+ WHERE realtime.account_id = account_id_value
533
+ AND realtime.workspace_id = workspace_id_value
534
+ AND realtime.owner_subject_id = target_subject
535
+ AND realtime.state = 'active'
536
+ ORDER BY realtime.id
537
+ FOR UPDATE;
538
+ SELECT COALESCE(pg_catalog.array_agg(realtime.id ORDER BY realtime.id), ARRAY[]::uuid[])
539
+ INTO affected_realtime_ids
540
+ FROM session_realtime_modes realtime
541
+ WHERE realtime.account_id = account_id_value
542
+ AND realtime.workspace_id = workspace_id_value
543
+ AND realtime.owner_subject_id = target_subject
544
+ AND realtime.state = 'active';
545
+ PERFORM 1 FROM session_realtime_connections connection
546
+ WHERE connection.account_id = account_id_value
547
+ AND connection.workspace_id = workspace_id_value
548
+ AND connection.realtime_id = ANY(affected_realtime_ids)
549
+ AND connection.state IN ('negotiating', 'ready', 'active')
550
+ ORDER BY connection.id
551
+ FOR UPDATE;
552
+
553
+ -- Pending tool calls with no live attempt need canonical protocol
554
+ -- settlement first, exactly as the organization lifecycle requires.
555
+ IF EXISTS (
556
+ SELECT 1
557
+ FROM session_turns turn_row
558
+ JOIN sessions owned ON owned.id = turn_row.session_id
559
+ WHERE turn_row.account_id = account_id_value
560
+ AND turn_row.workspace_id = workspace_id_value
561
+ AND turn_row.status IN (
562
+ 'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
563
+ )
564
+ AND (
565
+ (target_membership_id IS NOT NULL
566
+ AND owned.owner_organization_membership_id = target_membership_id)
567
+ OR turn_row.initiating_human_subject_id = target_subject
568
+ )
569
+ AND EXISTS (
570
+ SELECT 1 FROM session_pending_tool_calls pending
571
+ WHERE pending.account_id = turn_row.account_id
572
+ AND pending.workspace_id = turn_row.workspace_id
573
+ AND pending.session_id = turn_row.session_id
574
+ AND pending.turn_id = turn_row.id
575
+ )
576
+ AND NOT EXISTS (
577
+ SELECT 1 FROM session_turn_attempts live_attempt
578
+ WHERE live_attempt.account_id = turn_row.account_id
579
+ AND live_attempt.workspace_id = turn_row.workspace_id
580
+ AND live_attempt.session_id = turn_row.session_id
581
+ AND live_attempt.turn_id = turn_row.id
582
+ AND live_attempt.id = turn_row.active_attempt_id
583
+ AND live_attempt.state IN ('claimed', 'running')
584
+ )
585
+ ) THEN
586
+ RAISE EXCEPTION 'pending tool calls require canonical protocol settlement'
587
+ USING ERRCODE = '55000';
588
+ END IF;
589
+
590
+ SELECT COALESCE(pg_catalog.array_agg(DISTINCT turn_row.session_id), ARRAY[]::uuid[])
591
+ INTO affected_session_ids
592
+ FROM session_turns turn_row
593
+ JOIN sessions owned
594
+ ON owned.account_id = turn_row.account_id
595
+ AND owned.workspace_id = turn_row.workspace_id
596
+ AND owned.id = turn_row.session_id
597
+ WHERE turn_row.account_id = account_id_value
598
+ AND turn_row.workspace_id = workspace_id_value
599
+ AND turn_row.status IN (
600
+ 'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
601
+ )
602
+ AND (
603
+ (target_membership_id IS NOT NULL
604
+ AND owned.owner_organization_membership_id = target_membership_id)
605
+ OR turn_row.initiating_human_subject_id = target_subject
606
+ );
607
+ affected_session_ids := ARRAY(
608
+ SELECT DISTINCT candidate.session_id
609
+ FROM (
610
+ SELECT unnest(affected_session_ids) AS session_id
611
+ UNION ALL
612
+ SELECT realtime.session_id
613
+ FROM session_realtime_modes realtime
614
+ WHERE realtime.id = ANY(affected_realtime_ids)
615
+ ) candidate
616
+ ORDER BY candidate.session_id
617
+ );
618
+
619
+ interruption_operation_id := pg_catalog.gen_random_uuid();
620
+ INSERT INTO session_command_receipts (
621
+ id, account_id, workspace_id, actor_type, actor_subject_id,
622
+ action, operation_key, canonical_request_hash, result
623
+ ) VALUES (
624
+ interruption_operation_id, account_id_value, workspace_id_value,
625
+ 'human', actor_subject, 'workspace.membership.remove',
626
+ 'workspace-membership:' || operation_id_value::text,
627
+ input_hash_value,
628
+ pg_catalog.jsonb_build_object(
629
+ 'workspaceMembershipOperationId', operation_id_value,
630
+ 'workspaceMembershipId', membership_row_id,
631
+ 'targetSubjectId', target_subject
632
+ )
633
+ );
634
+ INSERT INTO session_attempt_interruptions (
635
+ account_id, workspace_id, session_id, operation_id, attempt_id,
636
+ kind, control_revision
637
+ )
638
+ SELECT attempt.account_id, attempt.workspace_id, attempt.session_id,
639
+ interruption_operation_id, attempt.id, 'authority_change',
640
+ CASE WHEN target_membership_id IS NOT NULL
641
+ AND owned.owner_organization_membership_id = target_membership_id
642
+ THEN owned.authority_epoch + 1 ELSE owned.authority_epoch END
643
+ FROM session_turn_attempts attempt
644
+ JOIN sessions owned ON owned.id = attempt.session_id
645
+ JOIN session_turns initiated ON initiated.id = attempt.turn_id
646
+ WHERE attempt.account_id = account_id_value
647
+ AND attempt.workspace_id = workspace_id_value
648
+ AND (
649
+ (target_membership_id IS NOT NULL
650
+ AND owned.owner_organization_membership_id = target_membership_id)
651
+ OR initiated.initiating_human_subject_id = target_subject
652
+ )
653
+ AND attempt.state IN ('claimed', 'running')
654
+ ON CONFLICT ON CONSTRAINT session_attempt_interruptions_operation_attempt_uq
655
+ DO NOTHING;
656
+
657
+ WITH cancelled AS (
658
+ UPDATE session_turns turn_row
659
+ SET status = 'cancelled', active_attempt_id = NULL,
660
+ cancelled_by = actor_subject, cancel_reason = 'authority_changed',
661
+ finished_at = now_value, updated_at = now_value,
662
+ version = turn_row.version + 1
663
+ WHERE turn_row.account_id = account_id_value
664
+ AND turn_row.workspace_id = workspace_id_value
665
+ AND turn_row.status IN (
666
+ 'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
667
+ )
668
+ AND EXISTS (
669
+ SELECT 1 FROM sessions owned
670
+ WHERE owned.account_id = turn_row.account_id
671
+ AND owned.workspace_id = turn_row.workspace_id
672
+ AND owned.id = turn_row.session_id
673
+ AND (
674
+ (target_membership_id IS NOT NULL
675
+ AND owned.owner_organization_membership_id = target_membership_id)
676
+ OR turn_row.initiating_human_subject_id = target_subject
677
+ )
678
+ )
679
+ AND NOT EXISTS (
680
+ SELECT 1 FROM session_turn_attempts live_attempt
681
+ WHERE live_attempt.account_id = turn_row.account_id
682
+ AND live_attempt.workspace_id = turn_row.workspace_id
683
+ AND live_attempt.session_id = turn_row.session_id
684
+ AND live_attempt.turn_id = turn_row.id
685
+ AND live_attempt.id = turn_row.active_attempt_id
686
+ AND live_attempt.state IN ('claimed', 'running')
687
+ )
688
+ RETURNING turn_row.id
689
+ )
690
+ SELECT COALESCE(pg_catalog.array_agg(cancelled.id ORDER BY cancelled.id), ARRAY[]::uuid[])
691
+ INTO cancelled_turn_ids
692
+ FROM cancelled;
693
+
694
+ FOR cancelled_turn IN
695
+ SELECT turn_row.id, turn_row.session_id, turn_row.execution_generation
696
+ FROM session_turns turn_row
697
+ WHERE turn_row.account_id = account_id_value
698
+ AND turn_row.workspace_id = workspace_id_value
699
+ AND turn_row.id = ANY(cancelled_turn_ids)
700
+ ORDER BY turn_row.session_id, turn_row.id
701
+ LOOP
702
+ SELECT affected.last_sequence,
703
+ CASE WHEN affected.active_turn_id = cancelled_turn.id
704
+ THEN 'current' ELSE NULL END
705
+ INTO cancelled_sequence, cancelled_association
706
+ FROM sessions affected
707
+ WHERE affected.account_id = account_id_value
708
+ AND affected.workspace_id = workspace_id_value
709
+ AND affected.id = cancelled_turn.session_id;
710
+ FOR cancelled_request IN
711
+ UPDATE session_human_input_requests request_row
712
+ SET status = 'cancelled', response = '{"outcome":"cancelled"}'::jsonb,
713
+ responded_by = actor_subject, responded_at = now_value,
714
+ updated_at = now_value
715
+ WHERE request_row.account_id = account_id_value
716
+ AND request_row.workspace_id = workspace_id_value
717
+ AND request_row.session_id = cancelled_turn.session_id
718
+ AND request_row.turn_id = cancelled_turn.id
719
+ AND request_row.status = 'pending'
720
+ RETURNING request_row.id, request_row.creation_attempt_id
721
+ LOOP
722
+ cancelled_sequence := cancelled_sequence + 1;
723
+ INSERT INTO session_events (
724
+ account_id, workspace_id, session_id, sequence, type, payload,
725
+ payload_codec_version, turn_id, turn_generation,
726
+ turn_attempt_id, turn_association, occurred_at
727
+ ) VALUES (
728
+ account_id_value, workspace_id_value, cancelled_turn.session_id,
729
+ cancelled_sequence, 'user.humanInputResponse',
730
+ pg_catalog.jsonb_build_object(
731
+ 'requestId', cancelled_request.id,
732
+ 'response', pg_catalog.jsonb_build_object('outcome', 'cancelled')
733
+ ), 1, cancelled_turn.id, cancelled_turn.execution_generation,
734
+ cancelled_request.creation_attempt_id, cancelled_association, now_value
735
+ );
736
+ END LOOP;
737
+ SELECT
738
+ pg_catalog.array_agg(
739
+ update_row.id ORDER BY update_row.created_at, update_row.id
740
+ ),
741
+ (pg_catalog.array_agg(
742
+ update_row.delivered_history_item_id
743
+ ORDER BY update_row.created_at, update_row.id
744
+ ))[1]
745
+ INTO settled_update_ids, settled_history_item_id
746
+ FROM session_system_updates update_row
747
+ WHERE update_row.account_id = account_id_value
748
+ AND update_row.workspace_id = workspace_id_value
749
+ AND update_row.session_id = cancelled_turn.session_id
750
+ AND update_row.delivered_turn_id = cancelled_turn.id
751
+ AND update_row.state = 'delivered';
752
+ IF COALESCE(pg_catalog.array_length(settled_update_ids, 1), 0) > 0
753
+ AND NOT EXISTS (
754
+ SELECT 1 FROM session_events prior_settlement
755
+ WHERE prior_settlement.account_id = account_id_value
756
+ AND prior_settlement.workspace_id = workspace_id_value
757
+ AND prior_settlement.session_id = cancelled_turn.session_id
758
+ AND prior_settlement.turn_id = cancelled_turn.id
759
+ AND prior_settlement.type = 'system.update.settled'
760
+ )
761
+ THEN
762
+ cancelled_sequence := cancelled_sequence + 1;
763
+ INSERT INTO session_events (
764
+ account_id, workspace_id, session_id, sequence, type, payload,
765
+ payload_codec_version, turn_id, turn_generation,
766
+ turn_association, occurred_at
767
+ ) VALUES (
768
+ account_id_value, workspace_id_value, cancelled_turn.session_id,
769
+ cancelled_sequence, 'system.update.settled',
770
+ pg_catalog.jsonb_build_object(
771
+ 'updateIds', pg_catalog.to_jsonb(settled_update_ids),
772
+ 'count', pg_catalog.array_length(settled_update_ids, 1),
773
+ 'historyItemId', settled_history_item_id,
774
+ 'outcome', 'cancelled'
775
+ ), 1, cancelled_turn.id, cancelled_turn.execution_generation,
776
+ cancelled_association, now_value
777
+ );
778
+ END IF;
779
+ IF NOT EXISTS (
780
+ SELECT 1 FROM session_events prior_cancel
781
+ WHERE prior_cancel.account_id = account_id_value
782
+ AND prior_cancel.workspace_id = workspace_id_value
783
+ AND prior_cancel.session_id = cancelled_turn.session_id
784
+ AND prior_cancel.turn_id = cancelled_turn.id
785
+ AND prior_cancel.type = 'turn.cancelled'
786
+ ) THEN
787
+ cancelled_sequence := cancelled_sequence + 1;
788
+ INSERT INTO session_events (
789
+ account_id, workspace_id, session_id, sequence, type, payload,
790
+ payload_codec_version, turn_id, turn_generation,
791
+ turn_association, occurred_at
792
+ ) VALUES (
793
+ account_id_value, workspace_id_value, cancelled_turn.session_id,
794
+ cancelled_sequence, 'turn.cancelled',
795
+ pg_catalog.jsonb_build_object('reason', 'authority_changed'),
796
+ 1, cancelled_turn.id, cancelled_turn.execution_generation,
797
+ cancelled_association, now_value
798
+ );
799
+ END IF;
800
+ UPDATE sessions affected
801
+ SET last_sequence = cancelled_sequence, updated_at = now_value
802
+ WHERE affected.account_id = account_id_value
803
+ AND affected.workspace_id = workspace_id_value
804
+ AND affected.id = cancelled_turn.session_id;
805
+ END LOOP;
806
+
807
+ UPDATE interaction_interventions intervention
808
+ SET status = 'cancelled', response_actor_subject_id = actor_subject,
809
+ version = intervention.version + 1, settled_at = now_value,
810
+ updated_at = now_value
811
+ WHERE intervention.account_id = account_id_value
812
+ AND intervention.workspace_id = workspace_id_value
813
+ AND intervention.originating_turn_id = ANY(cancelled_turn_ids)
814
+ AND intervention.status = 'open';
815
+ DELETE FROM agent_run_states run_state
816
+ WHERE run_state.account_id = account_id_value
817
+ AND run_state.workspace_id = workspace_id_value
818
+ AND run_state.turn_id = ANY(cancelled_turn_ids);
819
+ UPDATE codex_capacity_waiters waiter
820
+ SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
821
+ last_wake_reason = 'workspace_membership_remove',
822
+ updated_at = now_value
823
+ WHERE waiter.account_id = account_id_value
824
+ AND waiter.workspace_id = workspace_id_value
825
+ AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
826
+ AND waiter.status = 'waiting';
827
+ UPDATE xai_capacity_waiters waiter
828
+ SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
829
+ last_wake_reason = 'workspace_membership_remove',
830
+ updated_at = now_value
831
+ WHERE waiter.account_id = account_id_value
832
+ AND waiter.workspace_id = workspace_id_value
833
+ AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
834
+ AND waiter.status = 'waiting';
835
+ UPDATE session_realtime_connections connection
836
+ SET state = 'closed', closed_at = now_value, updated_at = now_value
837
+ WHERE connection.account_id = account_id_value
838
+ AND connection.workspace_id = workspace_id_value
839
+ AND connection.realtime_id = ANY(affected_realtime_ids)
840
+ AND connection.state IN ('negotiating', 'ready', 'active');
841
+ WITH ended AS (
842
+ UPDATE session_realtime_modes realtime
843
+ SET state = 'ended', version = realtime.version + 1,
844
+ ended_at = now_value, end_reason = 'authority_revoked',
845
+ updated_at = now_value
846
+ WHERE realtime.account_id = account_id_value
847
+ AND realtime.workspace_id = workspace_id_value
848
+ AND realtime.id = ANY(affected_realtime_ids)
849
+ AND realtime.state = 'active'
850
+ RETURNING realtime.id, realtime.session_id, realtime.operation_id,
851
+ realtime.version, realtime.connection_epoch
852
+ ), advanced AS (
853
+ UPDATE sessions affected
854
+ SET last_sequence = affected.last_sequence + 1, updated_at = now_value
855
+ FROM ended
856
+ WHERE affected.account_id = account_id_value
857
+ AND affected.workspace_id = workspace_id_value
858
+ AND affected.id = ended.session_id
859
+ RETURNING affected.id AS session_id, affected.last_sequence,
860
+ ended.id AS realtime_id, ended.operation_id,
861
+ ended.version, ended.connection_epoch
862
+ )
863
+ INSERT INTO session_events (
864
+ account_id, workspace_id, session_id, sequence, type, payload, occurred_at
865
+ )
866
+ SELECT account_id_value, workspace_id_value, advanced.session_id,
867
+ advanced.last_sequence, 'session.realtime.ended',
868
+ pg_catalog.jsonb_build_object(
869
+ 'realtimeId', advanced.realtime_id,
870
+ 'operationId', advanced.operation_id,
871
+ 'reason', 'authority_revoked',
872
+ 'version', advanced.version,
873
+ 'connectionEpoch', advanced.connection_epoch
874
+ ), now_value
875
+ FROM advanced;
876
+ UPDATE sessions affected
877
+ SET active_turn_id = CASE
878
+ WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN NULL
879
+ ELSE affected.active_turn_id
880
+ END,
881
+ status = CASE
882
+ WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN
883
+ CASE WHEN EXISTS (
884
+ SELECT 1 FROM session_turns queued
885
+ WHERE queued.account_id = affected.account_id
886
+ AND queued.workspace_id = affected.workspace_id
887
+ AND queued.session_id = affected.id
888
+ AND queued.status = 'queued'
889
+ ) THEN 'queued' ELSE 'idle' END
890
+ ELSE affected.status
891
+ END,
892
+ queue_head_position = COALESCE((
893
+ SELECT min(queued.position)::bigint FROM session_turns queued
894
+ WHERE queued.account_id = affected.account_id
895
+ AND queued.workspace_id = affected.workspace_id
896
+ AND queued.session_id = affected.id
897
+ AND queued.status = 'queued'
898
+ ), 0),
899
+ queue_tail_position = COALESCE((
900
+ SELECT max(queued.position)::bigint FROM session_turns queued
901
+ WHERE queued.account_id = affected.account_id
902
+ AND queued.workspace_id = affected.workspace_id
903
+ AND queued.session_id = affected.id
904
+ AND queued.status = 'queued'
905
+ ), 0),
906
+ queue_version = affected.queue_version + 1,
907
+ updated_at = now_value
908
+ WHERE affected.account_id = account_id_value
909
+ AND affected.workspace_id = workspace_id_value
910
+ AND EXISTS (
911
+ SELECT 1 FROM session_turns cancelled
912
+ WHERE cancelled.account_id = affected.account_id
913
+ AND cancelled.workspace_id = affected.workspace_id
914
+ AND cancelled.session_id = affected.id
915
+ AND cancelled.id = ANY(cancelled_turn_ids)
916
+ );
917
+ UPDATE session_system_updates update_row SET state = 'cancelled'
918
+ WHERE update_row.account_id = account_id_value
919
+ AND update_row.workspace_id = workspace_id_value
920
+ AND update_row.state = 'pending'
921
+ AND target_membership_id IS NOT NULL
922
+ AND EXISTS (
923
+ SELECT 1 FROM sessions owned
924
+ WHERE owned.id = update_row.session_id
925
+ AND owned.owner_organization_membership_id = target_membership_id
926
+ );
927
+ UPDATE session_system_update_outbox outbox
928
+ SET status = 'failed', last_error = 'workspace_membership_remove',
929
+ updated_at = now_value
930
+ WHERE outbox.account_id = account_id_value
931
+ AND outbox.workspace_id = workspace_id_value
932
+ AND outbox.status = 'pending'
933
+ AND target_membership_id IS NOT NULL
934
+ AND EXISTS (
935
+ SELECT 1 FROM sessions source_session
936
+ WHERE source_session.id = outbox.source_session_id
937
+ AND source_session.owner_organization_membership_id = target_membership_id
938
+ );
939
+ UPDATE session_goals goal_row
940
+ SET status = 'paused', paused_reason = 'api',
941
+ rationale = 'Workspace membership was removed; explicit reauthorization is required.',
942
+ version = goal_row.version + 1, updated_at = now_value
943
+ WHERE goal_row.account_id = account_id_value
944
+ AND goal_row.workspace_id = workspace_id_value
945
+ AND goal_row.status = 'active'
946
+ AND target_membership_id IS NOT NULL
947
+ AND EXISTS (
948
+ SELECT 1 FROM sessions owned
949
+ WHERE owned.id = goal_row.session_id
950
+ AND owned.owner_organization_membership_id = target_membership_id
951
+ );
952
+ UPDATE scheduled_tasks task
953
+ SET status = 'paused', personal_connection_delegations = '[]'::jsonb,
954
+ authority_revision = task.authority_revision + 1,
955
+ updated_at = now_value
956
+ WHERE task.account_id = account_id_value
957
+ AND task.workspace_id = workspace_id_value
958
+ AND task.deleted_at IS NULL
959
+ AND task.status = 'active'
960
+ AND (
961
+ (task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
962
+ OR EXISTS (
963
+ SELECT 1 FROM scheduled_task_revision_authorities authority
964
+ WHERE authority.task_id = task.id
965
+ AND authority.task_authority_revision = task.authority_revision
966
+ AND authority.subject_id = target_subject
967
+ )
968
+ OR EXISTS (
969
+ SELECT 1 FROM scheduled_task_personal_resource_authorities authority
970
+ WHERE authority.task_id = task.id
971
+ AND authority.task_authority_revision = task.authority_revision
972
+ AND authority.initiating_human_subject_id = target_subject
973
+ )
974
+ OR EXISTS (
975
+ SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
976
+ WHERE snapshot.task_id = task.id
977
+ AND snapshot.task_authority_revision = task.authority_revision
978
+ AND snapshot.owner_subject_id = target_subject
979
+ )
980
+ );
981
+ UPDATE organization_user_resource_grants grant_row
982
+ SET status = 'revoked', revoked_at = now_value,
983
+ generation = grant_row.generation + 1, updated_at = now_value
984
+ WHERE grant_row.account_id = account_id_value
985
+ AND grant_row.workspace_id = workspace_id_value
986
+ AND grant_row.status = 'active'
987
+ AND target_membership_id IS NOT NULL
988
+ AND grant_row.owner_organization_membership_id = target_membership_id;
989
+ UPDATE organization_user_resource_grants grant_row
990
+ SET status = 'revoked', revoked_at = now_value,
991
+ generation = grant_row.generation + 1, updated_at = now_value
992
+ WHERE grant_row.account_id = account_id_value
993
+ AND grant_row.workspace_id = workspace_id_value
994
+ AND grant_row.status = 'active'
995
+ AND target_membership_id IS NOT NULL
996
+ AND EXISTS (
997
+ SELECT 1 FROM sessions owned
998
+ WHERE owned.id = grant_row.session_id
999
+ AND owned.owner_organization_membership_id = target_membership_id
1000
+ AND owned.authority_epoch = grant_row.authority_epoch
1001
+ );
1002
+ IF target_membership_id IS NOT NULL THEN
1003
+ WITH advanced AS (
1004
+ UPDATE sessions owned SET
1005
+ authority_epoch = owned.authority_epoch + 1,
1006
+ initial_personal_connection_delegations = '[]'::jsonb,
1007
+ last_sequence = owned.last_sequence + 1
1008
+ WHERE owned.account_id = account_id_value
1009
+ AND owned.workspace_id = workspace_id_value
1010
+ AND owned.owner_organization_membership_id = target_membership_id
1011
+ RETURNING owned.id, owned.workspace_id, owned.last_sequence,
1012
+ owned.authority_epoch
1013
+ )
1014
+ INSERT INTO session_events (
1015
+ account_id, workspace_id, session_id, sequence, type, payload, occurred_at
1016
+ )
1017
+ SELECT account_id_value, advanced.workspace_id, advanced.id,
1018
+ advanced.last_sequence, 'session.authority.revoked',
1019
+ pg_catalog.jsonb_build_object(
1020
+ 'operationId', operation_id_value,
1021
+ 'organizationMembershipId', target_membership_id,
1022
+ 'previousAuthorityEpoch', advanced.authority_epoch - 1,
1023
+ 'authorityEpoch', advanced.authority_epoch
1024
+ ), now_value
1025
+ FROM advanced;
1026
+ END IF;
1027
+ INSERT INTO session_workflow_wake_outbox (
1028
+ session_id, account_id, workspace_id, temporal_workflow_id,
1029
+ reason, control_revision
1030
+ )
1031
+ SELECT affected.id, affected.account_id, affected.workspace_id,
1032
+ COALESCE(affected.temporal_workflow_id, 'session-' || affected.id::text),
1033
+ 'workspace_membership_remove', 1
1034
+ FROM sessions affected
1035
+ WHERE affected.account_id = account_id_value
1036
+ AND affected.workspace_id = workspace_id_value
1037
+ AND affected.id = ANY(affected_session_ids)
1038
+ ON CONFLICT (session_id) DO UPDATE SET
1039
+ wake_revision = session_workflow_wake_outbox.wake_revision + 1,
1040
+ control_revision = session_workflow_wake_outbox.wake_revision + 1,
1041
+ temporal_workflow_id = EXCLUDED.temporal_workflow_id,
1042
+ reason = EXCLUDED.reason, attempts = 0,
1043
+ next_attempt_at = now_value, last_error = NULL,
1044
+ updated_at = now_value;
1045
+
1046
+ -- Personal per-workspace rows and the membership itself, then the activity
1047
+ -- gate finalization exactly as the organization lifecycle performs it.
1048
+ DELETE FROM session_list_snapshots snapshot
1049
+ WHERE snapshot.workspace_id = workspace_id_value
1050
+ AND snapshot.subject_id = target_subject;
1051
+ DELETE FROM session_pins pin
1052
+ WHERE pin.workspace_id = workspace_id_value
1053
+ AND pin.subject_id = target_subject;
1054
+ DELETE FROM new_session_drafts draft
1055
+ WHERE draft.workspace_id = workspace_id_value
1056
+ AND draft.subject_id = target_subject;
1057
+ DELETE FROM workspace_memberships membership
1058
+ WHERE membership.id = membership_row_id;
1059
+
1060
+ PERFORM pg_catalog.set_config(
1061
+ 'opengeni.session_activity_gate_state', 'preparing', true
1062
+ );
1063
+ SET CONSTRAINTS ALL IMMEDIATE;
1064
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
1065
+ sessions_activity_update_commit_guard DEFERRED;
1066
+ PERFORM pg_catalog.set_config(
1067
+ 'opengeni.session_activity_gate_state', 'finalizing', true
1068
+ );
1069
+ UPDATE workspace_session_activity_revisions counter
1070
+ SET revision = counter.revision + 1
1071
+ WHERE counter.workspace_id = workspace_id_value
1072
+ RETURNING counter.revision INTO activity_revision_value;
1073
+ IF activity_revision_value IS NULL THEN
1074
+ RAISE EXCEPTION 'workspace membership session activity counter is unavailable'
1075
+ USING ERRCODE = '55000';
1076
+ END IF;
1077
+ UPDATE sessions affected
1078
+ SET activity_revision = activity_revision_value,
1079
+ activity_revision_pending_xid = NULL
1080
+ WHERE affected.account_id = account_id_value
1081
+ AND affected.workspace_id = workspace_id_value
1082
+ AND affected.activity_revision_pending_xid
1083
+ = pg_catalog.pg_current_xact_id()::text::bigint;
1084
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
1085
+ sessions_activity_update_commit_guard IMMEDIATE;
1086
+
1087
+ DELETE FROM session_visibility_write_capabilities capability
1088
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
1089
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id()
1090
+ AND capability.capability_id = visibility_capability_id;
1091
+ PERFORM pg_catalog.set_config(
1092
+ 'opengeni.session_visibility_write_capability',
1093
+ coalesce(previous_visibility_capability, ''), true
1094
+ );
1095
+ PERFORM pg_catalog.set_config(
1096
+ 'opengeni.workspace_id', coalesce(previous_workspace, ''), true
1097
+ );
1098
+ PERFORM pg_catalog.set_config(
1099
+ 'opengeni.session_activity_gate_state',
1100
+ coalesce(previous_activity_gate_state, ''), true
1101
+ );
1102
+ PERFORM pg_catalog.set_config(
1103
+ 'opengeni.session_activity_gate_workspace_id',
1104
+ coalesce(previous_activity_gate_workspace, ''), true
1105
+ );
1106
+ RETURN pg_catalog.jsonb_build_object(
1107
+ 'removed', true,
1108
+ 'workspaceMembershipId', membership_row_id,
1109
+ 'cancelledTurnIds', pg_catalog.to_jsonb(cancelled_turn_ids),
1110
+ 'affectedSessionIds', pg_catalog.to_jsonb(affected_session_ids)
1111
+ );
1112
+ EXCEPTION WHEN OTHERS THEN
1113
+ PERFORM pg_catalog.set_config(
1114
+ 'opengeni.workspace_id', coalesce(previous_workspace, ''), true
1115
+ );
1116
+ PERFORM pg_catalog.set_config(
1117
+ 'opengeni.session_visibility_write_capability',
1118
+ coalesce(previous_visibility_capability, ''), true
1119
+ );
1120
+ PERFORM pg_catalog.set_config(
1121
+ 'opengeni.session_activity_gate_state',
1122
+ coalesce(previous_activity_gate_state, ''), true
1123
+ );
1124
+ PERFORM pg_catalog.set_config(
1125
+ 'opengeni.session_activity_gate_workspace_id',
1126
+ coalesce(previous_activity_gate_workspace, ''), true
1127
+ );
1128
+ RAISE;
1129
+ END
1130
+ $body$;
1131
+
1132
+ -- 4. Hardening + grants. Same posture as every lifecycle seam: pinned
1133
+ -- search_path, revoked from PUBLIC, EXECUTE granted only to the runtime role
1134
+ -- where it exists (role-free dedicated-schema/embedded replays skip it).
1135
+
1136
+ REVOKE ALL ON FUNCTION prepare_workspace_membership_removal_settlements(jsonb) FROM PUBLIC;
1137
+ REVOKE ALL ON FUNCTION workspace_membership_removal_command(jsonb) FROM PUBLIC;
1138
+ REVOKE ALL ON FUNCTION opengeni_private.assert_workspace_membership_removal_actor(
1139
+ uuid, uuid, text, text
1140
+ ) FROM PUBLIC;
1141
+
1142
+ DO $workspace_membership_removal_seam$
1143
+ DECLARE
1144
+ data_schema text := pg_catalog.current_schema();
1145
+ BEGIN
1146
+ EXECUTE format(
1147
+ 'ALTER FUNCTION %I.prepare_workspace_membership_removal_settlements(jsonb) '
1148
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1149
+ data_schema, data_schema
1150
+ );
1151
+ EXECUTE format(
1152
+ 'ALTER FUNCTION %I.workspace_membership_removal_command(jsonb) '
1153
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1154
+ data_schema, data_schema
1155
+ );
1156
+ EXECUTE format(
1157
+ 'ALTER FUNCTION opengeni_private.assert_workspace_membership_removal_actor'
1158
+ || '(uuid,uuid,text,text) SET search_path = pg_catalog, %I, pg_temp',
1159
+ data_schema
1160
+ );
1161
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1162
+ EXECUTE format(
1163
+ 'GRANT EXECUTE ON FUNCTION %I.prepare_workspace_membership_removal_settlements(jsonb) '
1164
+ || 'TO opengeni_app',
1165
+ data_schema
1166
+ );
1167
+ EXECUTE format(
1168
+ 'GRANT EXECUTE ON FUNCTION %I.workspace_membership_removal_command(jsonb) '
1169
+ || 'TO opengeni_app',
1170
+ data_schema
1171
+ );
1172
+ END IF;
1173
+ END
1174
+ $workspace_membership_removal_seam$;