@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,67 @@
1
+ // The personal-workspace pointer, in its own leaf module.
2
+ //
3
+ // Both the Slack routing resolvers and the Slack access-request path need it,
4
+ // and this keeps that from becoming an import cycle between them.
5
+ import { sql } from "drizzle-orm";
6
+
7
+ import { type Database } from "./database";
8
+ import { listSelfOrganizationMemberships } from "./organization-membership-lifecycle";
9
+ import { rlsSubjectIdOrEmpty } from "./workspace-authority";
10
+
11
+ /**
12
+ * The subject's own personal workspace in one organization, or null.
13
+ *
14
+ * Only an `active` membership counts, and only that membership's own
15
+ * `personalWorkspaceId` pointer is read - never a default workspace, a
16
+ * `created_by`, or current access.
17
+ *
18
+ * Non-`user:` subjects (API keys, delegated service principals, configured and
19
+ * local principals) can never own an organization membership, so they
20
+ * short-circuit rather than tripping `list_self_organization_memberships`'
21
+ * `42501` guard.
22
+ *
23
+ * ORACLE, NOT AN AUTHORIZATION. This answers "which workspace is this named
24
+ * subject's own" for whatever subject it is handed; it does not establish that
25
+ * the caller IS that subject. Pass only a subject authenticated out of band -
26
+ * for Slack, one named by a durable `slack_bot_user_links` row. Never pass a
27
+ * subject taken from a request payload.
28
+ */
29
+ export async function namedSubjectPersonalWorkspaceId(
30
+ db: Database,
31
+ input: { accountId: string; subjectId: string },
32
+ ): Promise<string | null> {
33
+ if (!input.subjectId.startsWith("user:")) return null;
34
+ // `listSelfOrganizationMemberships` opens its own scope and sets
35
+ // `opengeni.subject_id` to the probed subject. Under drizzle a nested
36
+ // transaction is a SAVEPOINT, and releasing a savepoint does NOT undo a
37
+ // transaction-local `set_config`, so without this the probed subject would
38
+ // leak out and silently re-scope the rest of a caller-owned transaction.
39
+ // `namedSubjectHasLiveWorkspaceAuthority` guards the same hazard the same
40
+ // way; empty string is the canonical "unset" because `current_subject_id()`
41
+ // is `nullif(current_setting(...), '')`.
42
+ const priorSubjectId = await rlsSubjectIdOrEmpty(db);
43
+ let completed = false;
44
+ try {
45
+ const memberships = await listSelfOrganizationMemberships(db, input.subjectId);
46
+ const membership = memberships.find(
47
+ (candidate) =>
48
+ candidate.status === "active" &&
49
+ candidate.organizationId === input.accountId &&
50
+ candidate.personalWorkspaceId !== null,
51
+ );
52
+ completed = true;
53
+ return membership?.personalWorkspaceId ?? null;
54
+ } finally {
55
+ const restore = db.execute(
56
+ sql`select set_config('opengeni.subject_id', ${priorSubjectId}, true)`,
57
+ );
58
+ if (completed) {
59
+ await restore;
60
+ } else {
61
+ // A failed statement can leave a caller-owned transaction aborted.
62
+ // Preserve the original diagnostic rather than replacing it with the
63
+ // restore's `25P02`.
64
+ await restore.catch(() => undefined);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Per-channel and per-DM Slack workspace routing reads and writes.
3
+ *
4
+ * Two tenancy scopes, not one relocated workspace:
5
+ *
6
+ * - **HOME** is the installation binding's `(accountId, workspaceId)`. It owns
7
+ * the `connections` row and bot credential, the bot post/update/delete
8
+ * ledgers, `slack_interaction_inbox`, `slack_bot_user_links`, App Home
9
+ * refreshes, reaction-summon settings, Slack task policy, and every table in
10
+ * this module. The bot client is ALWAYS built from HOME.
11
+ * - **TARGET** is the routed `(accountId, workspaceId)`. It owns the
12
+ * `slack_interactions` row, its action handles, its progress deliveries, the
13
+ * grant, the session, and every session event.
14
+ *
15
+ * Both sides always sit in one organization: `target_account_id = account_id`
16
+ * is a table CHECK.
17
+ *
18
+ * `resolveSlackTargetAuthority` - the only place a Slack request may reach a
19
+ * managed human's personal workspace - deliberately lives in `./index`
20
+ * alongside `getWorkspaceGrant`, `managedPersonalWorkspacePermissions`, and
21
+ * `namedSubjectHasLiveWorkspaceAuthority`, because no sibling module in this
22
+ * package imports `./index` and this one must not be the first.
23
+ */
24
+ import { and, eq, sql } from "drizzle-orm";
25
+
26
+ import { rawRows, withRlsContext, type Database } from "./database";
27
+ import { namedSubjectPersonalWorkspaceId } from "./slack-routing-personal-workspace";
28
+ import * as schema from "./schema";
29
+ import { workspaceMembershipPermissionsAllowSlackLink } from "./slack-user-link-access";
30
+
31
+ /** The installation binding's own tenancy: credential, identity, and inbox. */
32
+ export type SlackRouteHome = { accountId: string; workspaceId: string };
33
+
34
+ /** How a stored route was decided. `picker` is a human answering the Slack card. */
35
+ export type SlackRouteSource = "picker" | "admin";
36
+
37
+ export type SlackChannelRoute = {
38
+ id: string;
39
+ accountId: string;
40
+ workspaceId: string;
41
+ connectionId: string;
42
+ slackTeamId: string;
43
+ slackChannelId: string;
44
+ targetAccountId: string;
45
+ targetWorkspaceId: string;
46
+ decidedBySubjectId: string;
47
+ decidedBySlackUserId: string;
48
+ source: SlackRouteSource;
49
+ version: number;
50
+ createdAt: Date;
51
+ updatedAt: Date;
52
+ };
53
+
54
+ export type SlackUserDmRoute = Omit<SlackChannelRoute, "slackChannelId"> & {
55
+ slackUserId: string;
56
+ };
57
+
58
+ export type UpsertSlackChannelRouteInput = {
59
+ connectionId: string;
60
+ slackTeamId: string;
61
+ slackChannelId: string;
62
+ targetAccountId: string;
63
+ targetWorkspaceId: string;
64
+ decidedBySubjectId: string;
65
+ decidedBySlackUserId: string;
66
+ source: SlackRouteSource;
67
+ };
68
+
69
+ export type UpsertSlackUserDmRouteInput = Omit<UpsertSlackChannelRouteInput, "slackChannelId"> & {
70
+ slackUserId: string;
71
+ };
72
+
73
+ /** Ids only. Never content, never a subject. */
74
+ export type SlackInteractionTenancy = {
75
+ accountId: string;
76
+ workspaceId: string;
77
+ interactionId: string;
78
+ };
79
+
80
+ /** One workspace a Slack-linked subject may start work in. */
81
+ export type SlackRoutableWorkspace = {
82
+ accountId: string;
83
+ workspaceId: string;
84
+ label: string;
85
+ /** True for the subject's own personal workspace, which has no membership row. */
86
+ personal: boolean;
87
+ };
88
+
89
+ function mapChannelRoute(row: typeof schema.slackChannelRoutes.$inferSelect): SlackChannelRoute {
90
+ return {
91
+ id: row.id,
92
+ accountId: row.accountId,
93
+ workspaceId: row.workspaceId,
94
+ connectionId: row.connectionId,
95
+ slackTeamId: row.slackTeamId,
96
+ slackChannelId: row.slackChannelId,
97
+ targetAccountId: row.targetAccountId,
98
+ targetWorkspaceId: row.targetWorkspaceId,
99
+ decidedBySubjectId: row.decidedBySubjectId,
100
+ decidedBySlackUserId: row.decidedBySlackUserId,
101
+ source: row.source,
102
+ version: row.version,
103
+ createdAt: row.createdAt,
104
+ updatedAt: row.updatedAt,
105
+ };
106
+ }
107
+
108
+ function mapUserDmRoute(row: typeof schema.slackUserDmRoutes.$inferSelect): SlackUserDmRoute {
109
+ return {
110
+ id: row.id,
111
+ accountId: row.accountId,
112
+ workspaceId: row.workspaceId,
113
+ connectionId: row.connectionId,
114
+ slackTeamId: row.slackTeamId,
115
+ slackUserId: row.slackUserId,
116
+ targetAccountId: row.targetAccountId,
117
+ targetWorkspaceId: row.targetWorkspaceId,
118
+ decidedBySubjectId: row.decidedBySubjectId,
119
+ decidedBySlackUserId: row.decidedBySlackUserId,
120
+ source: row.source,
121
+ version: row.version,
122
+ createdAt: row.createdAt,
123
+ updatedAt: row.updatedAt,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Resolve which tenancy owns the `slack_interactions` row for one exact
129
+ * connection-scoped route key, through the content-free SECURITY DEFINER probe.
130
+ *
131
+ * Thread continuation must cross workspaces: `slack_interactions_route_uq` is
132
+ * `(connection_id, route_key)` and therefore connection-global, while every
133
+ * ordinary interaction read is workspace-fenced and would miss a thread whose
134
+ * interaction lives in a routed workspace.
135
+ *
136
+ * This returns IDS ONLY. It cannot widen visibility, because the caller must
137
+ * re-read the full row under the returned tenancy's own RLS - which is what
138
+ * `getSlackInteractionById` already does.
139
+ */
140
+ export async function probeSlackInteractionTenancy(
141
+ db: Database,
142
+ input: { accountId: string; connectionId: string; routeKey: string },
143
+ ): Promise<SlackInteractionTenancy | null> {
144
+ const rows = await rawRows<{
145
+ account_id: string;
146
+ workspace_id: string;
147
+ interaction_id: string;
148
+ }>(
149
+ db,
150
+ sql`select account_id, workspace_id, interaction_id
151
+ from opengeni_private.resolve_slack_interaction_tenancy(
152
+ ${input.accountId}::uuid,
153
+ ${input.connectionId}::uuid,
154
+ ${input.routeKey}
155
+ )`,
156
+ );
157
+ const row = rows[0];
158
+ return row
159
+ ? {
160
+ accountId: row.account_id,
161
+ workspaceId: row.workspace_id,
162
+ interactionId: row.interaction_id,
163
+ }
164
+ : null;
165
+ }
166
+
167
+ /**
168
+ * Which workspace owns one Slack action handle.
169
+ *
170
+ * A button click arrives on an inbox row that carries the installation's
171
+ * tenancy and names the handle only by id, while the handle lives in the
172
+ * workspace that owns its session. Content-free, ids only: the caller must
173
+ * re-read the full handle under the returned tenancy's own RLS, where every
174
+ * existing authorization check already lives.
175
+ */
176
+ export async function probeSlackActionHandleTenancy(
177
+ db: Database,
178
+ input: { accountId: string; connectionId: string; handleId: string },
179
+ ): Promise<SlackRouteHome | null> {
180
+ const rows = await rawRows<{ account_id: string; workspace_id: string }>(
181
+ db,
182
+ sql`select account_id, workspace_id
183
+ from opengeni_private.resolve_slack_action_handle_tenancy(
184
+ ${input.accountId}::uuid,
185
+ ${input.connectionId}::uuid,
186
+ ${input.handleId}::uuid
187
+ )`,
188
+ );
189
+ const row = rows[0];
190
+ return row ? { accountId: row.account_id, workspaceId: row.workspace_id } : null;
191
+ }
192
+
193
+ export async function getSlackChannelRoute(
194
+ db: Database,
195
+ home: SlackRouteHome,
196
+ input: { connectionId: string; slackChannelId: string },
197
+ ): Promise<SlackChannelRoute | null> {
198
+ return await withRlsContext(db, home, async (scopedDb) => {
199
+ const [row] = await scopedDb
200
+ .select()
201
+ .from(schema.slackChannelRoutes)
202
+ .where(
203
+ and(
204
+ eq(schema.slackChannelRoutes.accountId, home.accountId),
205
+ eq(schema.slackChannelRoutes.workspaceId, home.workspaceId),
206
+ eq(schema.slackChannelRoutes.connectionId, input.connectionId),
207
+ eq(schema.slackChannelRoutes.slackChannelId, input.slackChannelId),
208
+ ),
209
+ )
210
+ .limit(1);
211
+ return row ? mapChannelRoute(row) : null;
212
+ });
213
+ }
214
+
215
+ export async function getSlackUserDmRoute(
216
+ db: Database,
217
+ home: SlackRouteHome,
218
+ input: { connectionId: string; slackUserId: string },
219
+ ): Promise<SlackUserDmRoute | null> {
220
+ return await withRlsContext(db, home, async (scopedDb) => {
221
+ const [row] = await scopedDb
222
+ .select()
223
+ .from(schema.slackUserDmRoutes)
224
+ .where(
225
+ and(
226
+ eq(schema.slackUserDmRoutes.accountId, home.accountId),
227
+ eq(schema.slackUserDmRoutes.workspaceId, home.workspaceId),
228
+ eq(schema.slackUserDmRoutes.connectionId, input.connectionId),
229
+ eq(schema.slackUserDmRoutes.slackUserId, input.slackUserId),
230
+ ),
231
+ )
232
+ .limit(1);
233
+ return row ? mapUserDmRoute(row) : null;
234
+ });
235
+ }
236
+
237
+ /**
238
+ * The ask-once memory for a channel. A repeat write for the same
239
+ * `(connectionId, slackChannelId)` re-points the channel and bumps `version`;
240
+ * it never creates a second row, so a channel always has exactly one answer.
241
+ *
242
+ * The caller is responsible for having authorized `target*` for the deciding
243
+ * subject through `resolveSlackTargetAuthority` first - a stored route is
244
+ * memory, never authority, and every later use re-authorizes it live.
245
+ */
246
+ export async function upsertSlackChannelRoute(
247
+ db: Database,
248
+ home: SlackRouteHome,
249
+ input: UpsertSlackChannelRouteInput,
250
+ ): Promise<SlackChannelRoute> {
251
+ return await withRlsContext(db, home, async (scopedDb) => {
252
+ const [row] = await scopedDb
253
+ .insert(schema.slackChannelRoutes)
254
+ .values({
255
+ accountId: home.accountId,
256
+ workspaceId: home.workspaceId,
257
+ connectionId: input.connectionId,
258
+ slackTeamId: input.slackTeamId,
259
+ slackChannelId: input.slackChannelId,
260
+ targetAccountId: input.targetAccountId,
261
+ targetWorkspaceId: input.targetWorkspaceId,
262
+ decidedBySubjectId: input.decidedBySubjectId,
263
+ decidedBySlackUserId: input.decidedBySlackUserId,
264
+ source: input.source,
265
+ })
266
+ .onConflictDoUpdate({
267
+ target: [schema.slackChannelRoutes.connectionId, schema.slackChannelRoutes.slackChannelId],
268
+ set: {
269
+ slackTeamId: input.slackTeamId,
270
+ targetAccountId: input.targetAccountId,
271
+ targetWorkspaceId: input.targetWorkspaceId,
272
+ decidedBySubjectId: input.decidedBySubjectId,
273
+ decidedBySlackUserId: input.decidedBySlackUserId,
274
+ source: input.source,
275
+ version: sql`${schema.slackChannelRoutes.version} + 1`,
276
+ updatedAt: sql`now()`,
277
+ },
278
+ })
279
+ .returning();
280
+ if (!row) throw new Error("Slack channel route write returned no row");
281
+ return mapChannelRoute(row);
282
+ });
283
+ }
284
+
285
+ /** The ask-once memory for one Slack human's direct messages. */
286
+ export async function upsertSlackUserDmRoute(
287
+ db: Database,
288
+ home: SlackRouteHome,
289
+ input: UpsertSlackUserDmRouteInput,
290
+ ): Promise<SlackUserDmRoute> {
291
+ return await withRlsContext(db, home, async (scopedDb) => {
292
+ const [row] = await scopedDb
293
+ .insert(schema.slackUserDmRoutes)
294
+ .values({
295
+ accountId: home.accountId,
296
+ workspaceId: home.workspaceId,
297
+ connectionId: input.connectionId,
298
+ slackTeamId: input.slackTeamId,
299
+ slackUserId: input.slackUserId,
300
+ targetAccountId: input.targetAccountId,
301
+ targetWorkspaceId: input.targetWorkspaceId,
302
+ decidedBySubjectId: input.decidedBySubjectId,
303
+ decidedBySlackUserId: input.decidedBySlackUserId,
304
+ source: input.source,
305
+ })
306
+ .onConflictDoUpdate({
307
+ target: [schema.slackUserDmRoutes.connectionId, schema.slackUserDmRoutes.slackUserId],
308
+ set: {
309
+ slackTeamId: input.slackTeamId,
310
+ targetAccountId: input.targetAccountId,
311
+ targetWorkspaceId: input.targetWorkspaceId,
312
+ decidedBySubjectId: input.decidedBySubjectId,
313
+ decidedBySlackUserId: input.decidedBySlackUserId,
314
+ source: input.source,
315
+ version: sql`${schema.slackUserDmRoutes.version} + 1`,
316
+ updatedAt: sql`now()`,
317
+ },
318
+ })
319
+ .returning();
320
+ if (!row) throw new Error("Slack DM route write returned no row");
321
+ return mapUserDmRoute(row);
322
+ });
323
+ }
324
+
325
+ /** Clear a channel's stored answer; the next message asks again. */
326
+ export async function deleteSlackChannelRoute(
327
+ db: Database,
328
+ home: SlackRouteHome,
329
+ input: { connectionId: string; slackChannelId: string },
330
+ ): Promise<boolean> {
331
+ return await withRlsContext(db, home, async (scopedDb) => {
332
+ const removed = await scopedDb
333
+ .delete(schema.slackChannelRoutes)
334
+ .where(
335
+ and(
336
+ eq(schema.slackChannelRoutes.accountId, home.accountId),
337
+ eq(schema.slackChannelRoutes.workspaceId, home.workspaceId),
338
+ eq(schema.slackChannelRoutes.connectionId, input.connectionId),
339
+ eq(schema.slackChannelRoutes.slackChannelId, input.slackChannelId),
340
+ ),
341
+ )
342
+ .returning({ id: schema.slackChannelRoutes.id });
343
+ return removed.length > 0;
344
+ });
345
+ }
346
+
347
+ /** Clear one Slack human's stored DM answer; DMs fall back to the derived personal workspace. */
348
+ export async function deleteSlackUserDmRoute(
349
+ db: Database,
350
+ home: SlackRouteHome,
351
+ input: { connectionId: string; slackUserId: string },
352
+ ): Promise<boolean> {
353
+ return await withRlsContext(db, home, async (scopedDb) => {
354
+ const removed = await scopedDb
355
+ .delete(schema.slackUserDmRoutes)
356
+ .where(
357
+ and(
358
+ eq(schema.slackUserDmRoutes.accountId, home.accountId),
359
+ eq(schema.slackUserDmRoutes.workspaceId, home.workspaceId),
360
+ eq(schema.slackUserDmRoutes.connectionId, input.connectionId),
361
+ eq(schema.slackUserDmRoutes.slackUserId, input.slackUserId),
362
+ ),
363
+ )
364
+ .returning({ id: schema.slackUserDmRoutes.id });
365
+ return removed.length > 0;
366
+ });
367
+ }
368
+
369
+ /**
370
+ * THE workspaces one subject may start Slack work in, within one organization.
371
+ *
372
+ * This is deliberately one function so the "workspaces this subject may create
373
+ * sessions in" question has a single implementation that a future shared
374
+ * candidate-set contract can replace wholesale, rather than several call sites
375
+ * each re-deriving it slightly differently.
376
+ *
377
+ * Two independent sources, unioned:
378
+ *
379
+ * 1. `workspace_memberships` joined to `workspaces` in this account, filtered
380
+ * to memberships that actually permit starting work (`sessions:create`, or
381
+ * `workspace:admin` which implies it).
382
+ * 2. The subject's OWN personal workspace, from the active organization
383
+ * membership's `personalWorkspaceId` pointer.
384
+ *
385
+ * `listWorkspacesForSubject` is NOT usable here: it is a bare membership join
386
+ * with no permission filter, and it cannot see personal workspaces at all,
387
+ * because a managed human's personal workspace deliberately has no
388
+ * `workspace_memberships` row.
389
+ *
390
+ * The personal-workspace id is DERIVED from the membership pointer. It is never
391
+ * accepted from a Slack payload, a route row, or a caller argument.
392
+ *
393
+ * Ordered stably by label then workspace id, with plain code-unit comparison so
394
+ * two hosts with different ICU data cannot render a picker differently.
395
+ *
396
+ * ORACLE, NOT AN AUTHORIZATION. This answers "what could this named subject use"
397
+ * for whatever subject it is handed; it does not establish that the caller IS
398
+ * that subject. Pass only a subject authenticated out of band - for Slack, one
399
+ * named by a durable `slack_bot_user_links` row. Never pass a subject taken
400
+ * from a request payload.
401
+ */
402
+ export async function listNamedSubjectSlackRoutableWorkspaces(
403
+ db: Database,
404
+ input: { accountId: string; subjectId: string },
405
+ ): Promise<SlackRoutableWorkspace[]> {
406
+ const membershipRows = await db
407
+ .select({
408
+ workspaceId: schema.workspaces.id,
409
+ accountId: schema.workspaces.accountId,
410
+ name: schema.workspaces.name,
411
+ permissions: schema.workspaceMemberships.permissions,
412
+ })
413
+ .from(schema.workspaceMemberships)
414
+ .innerJoin(schema.workspaces, eq(schema.workspaceMemberships.workspaceId, schema.workspaces.id))
415
+ .where(
416
+ and(
417
+ eq(schema.workspaceMemberships.subjectId, input.subjectId),
418
+ eq(schema.workspaces.accountId, input.accountId),
419
+ ),
420
+ );
421
+
422
+ const candidates = new Map<string, SlackRoutableWorkspace>();
423
+ for (const row of membershipRows) {
424
+ if (!workspaceMembershipPermissionsAllowSlackLink(row.permissions)) continue;
425
+ candidates.set(row.workspaceId, {
426
+ accountId: row.accountId,
427
+ workspaceId: row.workspaceId,
428
+ label: row.name,
429
+ personal: false,
430
+ });
431
+ }
432
+
433
+ const personalWorkspaceId = await namedSubjectPersonalWorkspaceId(db, input);
434
+ if (personalWorkspaceId) {
435
+ const [row] = await db
436
+ .select({
437
+ id: schema.workspaces.id,
438
+ accountId: schema.workspaces.accountId,
439
+ name: schema.workspaces.name,
440
+ })
441
+ .from(schema.workspaces)
442
+ .where(
443
+ and(
444
+ eq(schema.workspaces.id, personalWorkspaceId),
445
+ eq(schema.workspaces.accountId, input.accountId),
446
+ ),
447
+ )
448
+ .limit(1);
449
+ if (row) {
450
+ candidates.set(row.id, {
451
+ accountId: row.accountId,
452
+ workspaceId: row.id,
453
+ label: row.name,
454
+ personal: true,
455
+ });
456
+ }
457
+ }
458
+
459
+ return [...candidates.values()].sort(
460
+ (left, right) =>
461
+ compareCodeUnits(left.label, right.label) ||
462
+ compareCodeUnits(left.workspaceId, right.workspaceId),
463
+ );
464
+ }
465
+
466
+ function compareCodeUnits(left: string, right: string): number {
467
+ if (left === right) return 0;
468
+ return left < right ? -1 : 1;
469
+ }
@@ -114,6 +114,11 @@ export const slackSharedTaskOrigins = pgTable(
114
114
  sourceThreadTs: text("source_thread_ts").notNull(),
115
115
  initiatingSlackUserId: text("initiating_slack_user_id").notNull(),
116
116
  policyRevisionId: uuid("policy_revision_id").notNull(),
117
+ // The Slack task policy is a home fact and the interaction is a routed one,
118
+ // so the frozen revision carries its own tenancy pair. Null on rows written
119
+ // before Slack workspace routing, where the two were always equal.
120
+ policyAccountId: uuid("policy_account_id"),
121
+ policyWorkspaceId: uuid("policy_workspace_id"),
117
122
  policyHash: text("policy_hash").notNull(),
118
123
  policyActivationVersion: bigint("policy_activation_version", { mode: "number" }).notNull(),
119
124
  publicationMode: text("publication_mode")
@@ -30,6 +30,14 @@ export type SlackSharedTaskOrigin = {
30
30
  sourceThreadTs: string;
31
31
  initiatingSlackUserId: string;
32
32
  policyRevisionId: string;
33
+ /**
34
+ * Home tenancy of the frozen policy revision. The row's own
35
+ * `accountId`/`workspaceId` are the routed task's, which is a different
36
+ * workspace once Slack workspace routing is on. Null on rows written before
37
+ * routing existed, where the two were always equal.
38
+ */
39
+ policyAccountId: string | null;
40
+ policyWorkspaceId: string | null;
33
41
  policyHash: string;
34
42
  policyActivationVersion: number;
35
43
  publicationMode: "never" | "approval_required" | "allow";
@@ -310,6 +318,16 @@ export async function saveSlackSharedTaskOrigin(
310
318
  .limit(1);
311
319
  if (!existing) throw new Error("Slack shared-task origin conflict could not be resolved");
312
320
  for (const [key, value] of Object.entries(input)) {
321
+ // A row written before Slack workspace routing carries no policy tenancy
322
+ // and implied its own, because the two could not differ then. A replay
323
+ // from a new image supplies it, which is not a change of evidence and
324
+ // must not be reported as one.
325
+ if (
326
+ (key === "policyAccountId" || key === "policyWorkspaceId") &&
327
+ existing[key as keyof OriginRow] === null
328
+ ) {
329
+ continue;
330
+ }
313
331
  if (existing[key as keyof OriginRow] !== value) {
314
332
  throw new SlackTaskPolicyInvalidOperationError(
315
333
  "Slack shared-task origin identity conflicted with immutable evidence",
@@ -9,8 +9,10 @@ import {
9
9
  import { and, asc, eq, gt, inArray, ne } from "drizzle-orm";
10
10
 
11
11
  import { type Database, withWorkspaceRls } from "./database";
12
+ import { namedSubjectPersonalWorkspaceId } from "./slack-routing-personal-workspace";
12
13
  import { withLosslessContentWriteVersion } from "./lossless-json";
13
14
  import * as schema from "./schema";
15
+ import { normalizeWorkspaceMembershipPermissions } from "./workspace-membership-permissions";
14
16
 
15
17
  type RequestRow = typeof schema.slackUserLinkAccessRequests.$inferSelect;
16
18
  type OperationKind = "request" | "cancel" | "approve" | "deny";
@@ -563,7 +565,21 @@ async function membershipAllowsSlackLink(database: Database, row: RequestRow) {
563
565
  ),
564
566
  )
565
567
  .limit(1);
566
- const permissions = membership?.permissions ?? [];
568
+ if (workspaceMembershipPermissionsAllowSlackLink(membership?.permissions)) return true;
569
+ // A managed human's own personal workspace deliberately has no
570
+ // `workspace_memberships` row, so a bare membership join denies its owner.
571
+ // Once a Slack direct message can land there, that would tell people to
572
+ // request access to their own workspace. The pointer is DERIVED from an
573
+ // active organization membership; it is never accepted from the request row.
574
+ const personalWorkspaceId = await namedSubjectPersonalWorkspaceId(database, {
575
+ accountId: row.accountId,
576
+ subjectId: row.subjectId,
577
+ });
578
+ return personalWorkspaceId === row.workspaceId;
579
+ }
580
+
581
+ export function workspaceMembershipPermissionsAllowSlackLink(value: unknown): boolean {
582
+ const permissions = normalizeWorkspaceMembershipPermissions(value);
567
583
  return permissions.includes("sessions:create") || permissions.includes("workspace:admin");
568
584
  }
569
585
 
@@ -133,3 +133,77 @@ export const taskNoteWriteCapabilities = pgTable(
133
133
  ),
134
134
  }),
135
135
  );
136
+
137
+ // Transaction-local, owner-only admission for the single Task-note evidence
138
+ // insert materialized by a governed promotion. Migration 0260 owns its RLS,
139
+ // one-shot trigger consumption, and runtime-role revocation.
140
+ export const taskNoteKnowledgePromotionCapabilities = pgTable(
141
+ "task_note_knowledge_promotion_capabilities",
142
+ {
143
+ backendPid: integer("backend_pid").notNull(),
144
+ transactionId: xid8("transaction_id").notNull(),
145
+ capabilityId: uuid("capability_id").notNull(),
146
+ accountId: uuid("account_id").notNull(),
147
+ workspaceId: uuid("workspace_id").notNull(),
148
+ noteId: uuid("note_id").notNull(),
149
+ rootSessionId: uuid("root_session_id").notNull(),
150
+ noteVersion: integer("note_version").notNull(),
151
+ noteTextHash: text("note_text_hash").notNull(),
152
+ evidenceOperationId: text("evidence_operation_id").notNull(),
153
+ claimOperationId: text("claim_operation_id").notNull(),
154
+ actorSubjectId: text("actor_subject_id").notNull(),
155
+ initiatingHumanSubjectId: text("initiating_human_subject_id").notNull(),
156
+ learningPolicySnapshotId: uuid("learning_policy_snapshot_id").notNull(),
157
+ },
158
+ (table) => ({
159
+ identity: uniqueIndex("task_note_knowledge_promotion_capabilities_identity_uq").on(
160
+ table.backendPid,
161
+ table.transactionId,
162
+ table.capabilityId,
163
+ ),
164
+ }),
165
+ );
166
+
167
+ // Immutable, content-free lineage for one atomic archive + replacement create.
168
+ // Migration 0260 owns exact constraints, FORCE RLS, and lifecycle-only writes.
169
+ export const taskNoteReplacementReceipts = pgTable(
170
+ "task_note_replacement_receipts",
171
+ {
172
+ id: uuid("id").primaryKey().defaultRandom(),
173
+ accountId: uuid("account_id").notNull(),
174
+ workspaceId: uuid("workspace_id").notNull(),
175
+ rootSessionId: uuid("root_session_id").notNull(),
176
+ operationId: uuid("operation_id").notNull(),
177
+ inputHash: text("input_hash").notNull(),
178
+ replacedNoteId: uuid("replaced_note_id").notNull(),
179
+ replacedNoteVersion: integer("replaced_note_version").notNull(),
180
+ replacementNoteId: uuid("replacement_note_id").notNull(),
181
+ replacementNoteVersion: integer("replacement_note_version").notNull(),
182
+ archiveOperationId: uuid("archive_operation_id").notNull(),
183
+ createOperationId: uuid("create_operation_id").notNull(),
184
+ actorKind: text("actor_kind").notNull(),
185
+ actorSubjectId: text("actor_subject_id").notNull(),
186
+ initiatingHumanSubjectId: text("initiating_human_subject_id"),
187
+ actorSessionId: uuid("actor_session_id").notNull(),
188
+ actorTurnId: uuid("actor_turn_id").notNull(),
189
+ actorAttemptId: uuid("actor_attempt_id").notNull(),
190
+ actorExecutionGeneration: integer("actor_execution_generation").notNull(),
191
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
192
+ },
193
+ (table) => ({
194
+ workspaceOperation: uniqueIndex("task_note_replacement_receipts_workspace_operation_uq").on(
195
+ table.workspaceId,
196
+ table.operationId,
197
+ ),
198
+ workspaceReplacement: uniqueIndex("task_note_replacement_receipts_workspace_replacement_uq").on(
199
+ table.workspaceId,
200
+ table.replacementNoteId,
201
+ ),
202
+ rootTimeline: index("task_note_replacement_receipts_root_timeline_idx").on(
203
+ table.workspaceId,
204
+ table.rootSessionId,
205
+ table.createdAt,
206
+ table.id,
207
+ ),
208
+ }),
209
+ );