@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
@@ -9,9 +9,11 @@ import type {
9
9
  WorkspaceInstructionPolicyActivationEvent,
10
10
  WorkspaceInstructionPolicyActivationResponse,
11
11
  WorkspaceInstructionPolicyActivationType,
12
+ WorkspaceInstructionPolicyDeactivationEvent,
12
13
  WorkspaceInstructionPolicyDiffResponse,
13
14
  WorkspaceInstructionPolicyDraftProvenanceSource,
14
15
  WorkspaceInstructionPolicyHead,
16
+ WorkspaceInstructionPolicyInactiveHead,
15
17
  WorkspaceInstructionPolicyKind,
16
18
  WorkspaceInstructionPolicyListQuery,
17
19
  WorkspaceInstructionPolicyListResponse,
@@ -24,9 +26,14 @@ import type {
24
26
  WorkspaceInstructionPolicySnapshotEntry,
25
27
  WorkspaceInstructionPolicyTarget,
26
28
  } from "@opengeni/contracts";
27
- import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm";
29
+ import { and, asc, desc, eq, inArray, isNull, lt, notExists, or, sql, type SQL } from "drizzle-orm";
28
30
  import type { Database } from "./database";
29
- import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./database";
31
+ import {
32
+ withRlsContext,
33
+ withSessionRlsActorContext,
34
+ withWorkspaceRls,
35
+ withWorkspaceSubjectRls,
36
+ } from "./database";
30
37
  import { nestedPostgresSqlState } from "./persistence-errors";
31
38
  import * as schema from "./schema";
32
39
 
@@ -87,6 +94,19 @@ type OnboardingProposalInput = OnboardingProposalRequestInput & {
87
94
  requestFingerprint: string;
88
95
  };
89
96
 
97
+ export type WorkspaceInstructionPolicyKnowledgeProposalInput = WorkspaceInstructionPolicyTarget & {
98
+ operationId: string;
99
+ accountId: string;
100
+ workspaceId: string;
101
+ content: string;
102
+ knowledgeProposalId: string;
103
+ knowledgeProposalContentHash: string;
104
+ confidenceBps: number;
105
+ expectedCurrentRevisionId: string | null;
106
+ expectedActivationVersion: number;
107
+ createdBySubjectId: string;
108
+ };
109
+
90
110
  export class WorkspaceInstructionPolicyConflictError extends Error {
91
111
  readonly name = "WorkspaceInstructionPolicyConflictError";
92
112
  readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
@@ -141,6 +161,16 @@ export class WorkspaceInstructionPolicyOnboardingProposalContentError extends Er
141
161
  }
142
162
  }
143
163
 
164
+ /**
165
+ * The exact diagnostic `activate_human_confirmed_learning_decision` raises when
166
+ * a proposal's compare-and-set baseline no longer matches the live head. It is
167
+ * matched by message because the SQLSTATE (40001) is shared with other
168
+ * conflicts. Kept here so the SQL and the code that keys recovery off it cannot
169
+ * drift apart silently - the migration test pins the same string.
170
+ */
171
+ export const INSTRUCTION_POLICY_STALE_BASELINE_DIAGNOSTIC =
172
+ "instruction-policy proposal baseline is stale";
173
+
144
174
  export class WorkspaceInstructionPolicyOnboardingProposalStaleError extends Error {
145
175
  readonly name = "WorkspaceInstructionPolicyOnboardingProposalStaleError";
146
176
  readonly code = "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_STALE";
@@ -268,6 +298,14 @@ function activeRevisionRequestFingerprint(input: ActiveRevisionInput): string {
268
298
  ]);
269
299
  }
270
300
 
301
+ // The activation baseline is deliberately NOT part of either fingerprint. Its
302
+ // job is compare-and-set staleness detection at write time, not the identity of
303
+ // the request: two calls that ask for the same rule on the same target are the
304
+ // same operation even if the head moved between them. Hashing a live-read value
305
+ // into the identity made an ordinary turn-recovery replay of the same
306
+ // operationId fail as an operation-reuse conflict once a confirm advanced the
307
+ // head. Staleness is still enforced - by the compare-and-set below and by the
308
+ // activation function - it is just no longer conflated with request identity.
271
309
  function onboardingProposalRequestFingerprint(input: OnboardingProposalRequestInput): string {
272
310
  return operationRequestFingerprint("create_onboarding_proposal", [
273
311
  ["accountId", true, input.accountId],
@@ -279,8 +317,21 @@ function onboardingProposalRequestFingerprint(input: OnboardingProposalRequestIn
279
317
  ["sourceId", true, input.sourceId],
280
318
  ["sourceVersion", true, input.sourceVersion],
281
319
  ["confidenceBps", true, input.confidenceBps],
282
- ["expectedCurrentRevisionId", true, input.expectedCurrentRevisionId],
283
- ["expectedActivationVersion", true, input.expectedActivationVersion],
320
+ ["createdBySubjectId", true, input.createdBySubjectId],
321
+ ]);
322
+ }
323
+
324
+ function knowledgeProposalRequestFingerprint(input: OnboardingProposalRequestInput): string {
325
+ return operationRequestFingerprint("create_knowledge_proposal", [
326
+ ["accountId", true, input.accountId],
327
+ ["workspaceId", true, input.workspaceId],
328
+ ["kind", true, input.kind],
329
+ ["scope", true, input.scope],
330
+ ["roleKey", true, input.roleKey],
331
+ ["content", true, input.content],
332
+ ["knowledgeProposalId", true, input.sourceId],
333
+ ["knowledgeProposalContentHash", true, input.sourceVersion],
334
+ ["confidenceBps", true, input.confidenceBps],
284
335
  ["createdBySubjectId", true, input.createdBySubjectId],
285
336
  ]);
286
337
  }
@@ -420,6 +471,7 @@ export async function getWorkspaceStateAcceptedAttemptGovernance(
420
471
  type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
421
472
  type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
422
473
  type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
474
+ type DeactivationEventRow = typeof schema.workspaceInstructionPolicyDeactivationEvents.$inferSelect;
423
475
  type OnboardingProposalRow =
424
476
  typeof schema.workspaceInstructionPolicyOnboardingProposals.$inferSelect;
425
477
 
@@ -459,6 +511,17 @@ function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
459
511
  };
460
512
  }
461
513
 
514
+ function inactiveHeadFromRow(row: DeactivationEventRow): WorkspaceInstructionPolicyInactiveHead {
515
+ return {
516
+ workspaceId: row.workspaceId,
517
+ kind: row.kind as WorkspaceInstructionPolicyKind,
518
+ scope: row.scope as WorkspaceInstructionPolicyScope,
519
+ roleKey: row.roleKey,
520
+ activationVersion: row.activationVersion,
521
+ deactivatedAt: iso(row.createdAt),
522
+ };
523
+ }
524
+
462
525
  function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
463
526
  return {
464
527
  id: row.id,
@@ -489,6 +552,30 @@ function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent
489
552
  };
490
553
  }
491
554
 
555
+ function deactivationEventFromRow(
556
+ row: DeactivationEventRow,
557
+ ): WorkspaceInstructionPolicyDeactivationEvent {
558
+ return {
559
+ id: row.id,
560
+ operationId: row.operationId,
561
+ accountId: row.accountId,
562
+ workspaceId: row.workspaceId,
563
+ kind: row.kind as WorkspaceInstructionPolicyKind,
564
+ scope: row.scope as WorkspaceInstructionPolicyScope,
565
+ roleKey: row.roleKey,
566
+ type: "automatic_deactivate",
567
+ activationVersion: row.activationVersion,
568
+ oldRevision: {
569
+ id: row.oldRevisionId,
570
+ revision: row.oldRevision,
571
+ contentHash: row.oldContentHash,
572
+ },
573
+ actorSubjectId: row.actorSubjectId,
574
+ reason: row.reason,
575
+ createdAt: iso(row.createdAt),
576
+ };
577
+ }
578
+
492
579
  function onboardingProposalFromRow(
493
580
  row: OnboardingProposalRow,
494
581
  draftRow: RevisionRow,
@@ -569,6 +656,20 @@ function eventTargetConditions(
569
656
  ];
570
657
  }
571
658
 
659
+ function deactivationTargetConditions(
660
+ workspaceId: string,
661
+ target: WorkspaceInstructionPolicyTarget,
662
+ ): SQL[] {
663
+ return [
664
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.workspaceId, workspaceId),
665
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.kind, target.kind),
666
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.scope, target.scope),
667
+ target.roleKey === null
668
+ ? isNull(schema.workspaceInstructionPolicyDeactivationEvents.roleKey)
669
+ : eq(schema.workspaceInstructionPolicyDeactivationEvents.roleKey, target.roleKey),
670
+ ];
671
+ }
672
+
572
673
  async function lockWorkspace(
573
674
  db: Database,
574
675
  input: { accountId: string; workspaceId: string },
@@ -656,6 +757,24 @@ async function getEventByOperationInTransaction(
656
757
  return row ?? null;
657
758
  }
658
759
 
760
+ async function hasDeactivationOperationInTransaction(
761
+ db: Database,
762
+ workspaceId: string,
763
+ operationId: string,
764
+ ): Promise<boolean> {
765
+ const [row] = await db
766
+ .select({ id: schema.workspaceInstructionPolicyDeactivationEvents.id })
767
+ .from(schema.workspaceInstructionPolicyDeactivationEvents)
768
+ .where(
769
+ and(
770
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.workspaceId, workspaceId),
771
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.operationId, operationId),
772
+ ),
773
+ )
774
+ .limit(1);
775
+ return row !== undefined;
776
+ }
777
+
659
778
  async function getOnboardingProposalByOperationInTransaction(
660
779
  db: Database,
661
780
  workspaceId: string,
@@ -696,6 +815,11 @@ async function getOnboardingProposalBySourceVersionInTransaction(
696
815
  eq(schema.workspaceInstructionPolicyOnboardingProposals.sourceVersion, input.sourceVersion),
697
816
  ),
698
817
  )
818
+ .orderBy(
819
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.baselineActivationVersion),
820
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.createdAt),
821
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.id),
822
+ )
699
823
  .limit(1);
700
824
  return row ?? null;
701
825
  }
@@ -725,6 +849,23 @@ async function getHeadInTransaction(
725
849
  return row ?? null;
726
850
  }
727
851
 
852
+ async function getLatestDeactivationInTransaction(
853
+ db: Database,
854
+ workspaceId: string,
855
+ target: WorkspaceInstructionPolicyTarget,
856
+ ): Promise<DeactivationEventRow | null> {
857
+ const [row] = await db
858
+ .select()
859
+ .from(schema.workspaceInstructionPolicyDeactivationEvents)
860
+ .where(and(...deactivationTargetConditions(workspaceId, target)))
861
+ .orderBy(
862
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.activationVersion),
863
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.id),
864
+ )
865
+ .limit(1);
866
+ return row ?? null;
867
+ }
868
+
728
869
  function sameTarget(
729
870
  left: Pick<WorkspaceInstructionPolicyRevision, "kind" | "scope" | "roleKey">,
730
871
  right: WorkspaceInstructionPolicyTarget,
@@ -749,7 +890,10 @@ async function createDraftInTransaction(
749
890
  db: Database,
750
891
  input: DraftInput,
751
892
  ): Promise<WorkspaceInstructionPolicyRevision> {
752
- if (await getEventByOperationInTransaction(db, input.workspaceId, input.operationId)) {
893
+ if (
894
+ (await getEventByOperationInTransaction(db, input.workspaceId, input.operationId)) ||
895
+ (await hasDeactivationOperationInTransaction(db, input.workspaceId, input.operationId))
896
+ ) {
753
897
  throw new WorkspaceInstructionPolicyOperationReuseError();
754
898
  }
755
899
  const existing = await getRevisionByOperationInTransaction(
@@ -844,7 +988,16 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
844
988
  async (scopedDb) => {
845
989
  const workspace = await lockWorkspace(scopedDb, input);
846
990
  if (
847
- await getEventByOperationInTransaction(scopedDb, input.workspaceId, normalized.operationId)
991
+ (await getEventByOperationInTransaction(
992
+ scopedDb,
993
+ input.workspaceId,
994
+ normalized.operationId,
995
+ )) ||
996
+ (await hasDeactivationOperationInTransaction(
997
+ scopedDb,
998
+ input.workspaceId,
999
+ normalized.operationId,
1000
+ ))
848
1001
  ) {
849
1002
  throw new WorkspaceInstructionPolicyOperationReuseError();
850
1003
  }
@@ -893,30 +1046,12 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
893
1046
  );
894
1047
  }
895
1048
 
896
- export async function createWorkspaceInstructionPolicyOnboardingProposal(
1049
+ async function createWorkspaceInstructionPolicySourceProposal(
897
1050
  db: Database,
898
- input: Omit<OnboardingProposalRequestInput, "operationId"> & { operationId?: string },
1051
+ input: OnboardingProposalInput,
1052
+ draftProvenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource,
1053
+ draftProvenanceSourceId: (proposalId: string) => string,
899
1054
  ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
900
- if (input.content.trim().length === 0) {
901
- throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
902
- "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY",
903
- );
904
- }
905
- if (input.content.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS) {
906
- throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
907
- "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
908
- );
909
- }
910
- const request: OnboardingProposalRequestInput = {
911
- ...input,
912
- operationId: input.operationId ?? randomUUID(),
913
- sourceId: input.sourceId.trim(),
914
- sourceVersion: input.sourceVersion.trim(),
915
- };
916
- const normalized: OnboardingProposalInput = {
917
- ...request,
918
- requestFingerprint: onboardingProposalRequestFingerprint(request),
919
- };
920
1055
  return await withRlsContext(
921
1056
  db,
922
1057
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -925,48 +1060,58 @@ export async function createWorkspaceInstructionPolicyOnboardingProposal(
925
1060
  const existing = await getOnboardingProposalByOperationInTransaction(
926
1061
  scopedDb,
927
1062
  input.workspaceId,
928
- normalized.operationId,
1063
+ input.operationId,
929
1064
  );
930
1065
  if (existing) {
931
- if (existing.requestFingerprint !== normalized.requestFingerprint) {
1066
+ if (existing.requestFingerprint !== input.requestFingerprint) {
932
1067
  throw new WorkspaceInstructionPolicyOperationReuseError();
933
1068
  }
934
1069
  return await resolveOnboardingProposalInTransaction(scopedDb, existing);
935
1070
  }
936
1071
  if (
937
- (await getEventByOperationInTransaction(
1072
+ (await getEventByOperationInTransaction(scopedDb, input.workspaceId, input.operationId)) ||
1073
+ (await hasDeactivationOperationInTransaction(
938
1074
  scopedDb,
939
1075
  input.workspaceId,
940
- normalized.operationId,
1076
+ input.operationId,
941
1077
  )) ||
942
- (await getRevisionByOperationInTransaction(
943
- scopedDb,
944
- input.workspaceId,
945
- normalized.operationId,
946
- ))
1078
+ (await getRevisionByOperationInTransaction(scopedDb, input.workspaceId, input.operationId))
947
1079
  ) {
948
1080
  throw new WorkspaceInstructionPolicyOperationReuseError();
949
1081
  }
950
1082
 
951
1083
  const target: WorkspaceInstructionPolicyTarget = {
952
- kind: normalized.kind,
953
- scope: normalized.scope,
954
- roleKey: normalized.roleKey,
1084
+ kind: input.kind,
1085
+ scope: input.scope,
1086
+ roleKey: input.roleKey,
955
1087
  };
956
1088
  const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, target);
957
1089
  const currentHead = currentRow ? headFromRow(currentRow) : null;
1090
+ const inactiveBoundary = currentRow
1091
+ ? null
1092
+ : await getLatestDeactivationInTransaction(scopedDb, input.workspaceId, target);
1093
+ const currentActivationVersion =
1094
+ currentRow?.activationVersion ?? inactiveBoundary?.activationVersion ?? 0;
958
1095
  if (
959
- (currentHead?.revisionId ?? null) !== normalized.expectedCurrentRevisionId ||
960
- (currentHead?.activationVersion ?? 0) !== normalized.expectedActivationVersion
1096
+ (currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
1097
+ currentActivationVersion !== input.expectedActivationVersion
961
1098
  ) {
962
1099
  throw new WorkspaceInstructionPolicyOnboardingProposalStaleError(currentHead);
963
1100
  }
964
1101
 
1102
+ // A second proposal for the same knowledge proposal is legitimate when it
1103
+ // is a rebaselined successor - the confirm path adds one when the head
1104
+ // moved after the human already answered. Only a duplicate against the
1105
+ // same baseline is a conflict.
965
1106
  const sourceConflict = await getOnboardingProposalBySourceVersionInTransaction(
966
1107
  scopedDb,
967
- normalized,
1108
+ input,
968
1109
  );
969
- if (sourceConflict) {
1110
+ if (
1111
+ sourceConflict &&
1112
+ sourceConflict.baselineActivationVersion === currentActivationVersion &&
1113
+ (sourceConflict.baselineRevisionId ?? null) === (currentHead?.revisionId ?? null)
1114
+ ) {
970
1115
  throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
971
1116
  sourceConflict.id,
972
1117
  sourceConflict.draftRevisionId,
@@ -975,68 +1120,74 @@ export async function createWorkspaceInstructionPolicyOnboardingProposal(
975
1120
 
976
1121
  const proposalId = randomUUID();
977
1122
  const draft = await createDraftInTransaction(scopedDb, {
978
- operationId: normalized.operationId,
979
- requestFingerprint: normalized.requestFingerprint,
980
- accountId: normalized.accountId,
981
- workspaceId: normalized.workspaceId,
982
- kind: normalized.kind,
983
- scope: normalized.scope,
984
- roleKey: normalized.roleKey,
985
- content: normalized.content,
986
- provenanceSource: "onboarding",
987
- provenanceSourceId: proposalId,
1123
+ operationId: input.operationId,
1124
+ requestFingerprint: input.requestFingerprint,
1125
+ accountId: input.accountId,
1126
+ workspaceId: input.workspaceId,
1127
+ kind: input.kind,
1128
+ scope: input.scope,
1129
+ roleKey: input.roleKey,
1130
+ content: input.content,
1131
+ provenanceSource: draftProvenanceSource,
1132
+ provenanceSourceId: draftProvenanceSourceId(proposalId),
988
1133
  supersedesRevisionId: currentHead?.revisionId ?? null,
989
- createdBySubjectId: normalized.createdBySubjectId,
1134
+ createdBySubjectId: input.createdBySubjectId,
990
1135
  });
991
1136
  const createdAt = new Date();
992
1137
  const [created] = await scopedDb
993
1138
  .insert(schema.workspaceInstructionPolicyOnboardingProposals)
994
1139
  .values({
995
1140
  id: proposalId,
996
- operationId: normalized.operationId,
997
- requestFingerprint: normalized.requestFingerprint,
998
- accountId: normalized.accountId,
999
- workspaceId: normalized.workspaceId,
1000
- kind: normalized.kind,
1001
- scope: normalized.scope,
1002
- roleKey: normalized.roleKey,
1003
- sourceId: normalized.sourceId,
1004
- sourceVersion: normalized.sourceVersion,
1005
- confidenceBps: normalized.confidenceBps,
1141
+ operationId: input.operationId,
1142
+ requestFingerprint: input.requestFingerprint,
1143
+ accountId: input.accountId,
1144
+ workspaceId: input.workspaceId,
1145
+ kind: input.kind,
1146
+ scope: input.scope,
1147
+ roleKey: input.roleKey,
1148
+ sourceId: input.sourceId,
1149
+ sourceVersion: input.sourceVersion,
1150
+ confidenceBps: input.confidenceBps,
1006
1151
  baselineRevisionId: currentHead?.revisionId ?? null,
1007
1152
  baselineRevision: currentHead?.revision ?? null,
1008
1153
  baselineContentHash: currentHead?.contentHash ?? null,
1009
- baselineActivationVersion: currentHead?.activationVersion ?? 0,
1010
- baselineActivatedAt: currentHead ? new Date(currentHead.activatedAt) : null,
1154
+ baselineActivationVersion: currentActivationVersion,
1155
+ // The head's activated_at may carry microseconds (the governed
1156
+ // learning SQL controller writes clock_timestamp()), and the draft
1157
+ // trigger compares it exactly. A JS Date truncates to milliseconds,
1158
+ // so copy the stored value in SQL instead of round-tripping it.
1159
+ baselineActivatedAt: currentRow
1160
+ ? sql`(select ${schema.workspaceInstructionPolicyHeads.activatedAt} from ${schema.workspaceInstructionPolicyHeads} where ${schema.workspaceInstructionPolicyHeads.id} = ${currentRow.id})`
1161
+ : null,
1011
1162
  draftRevisionId: draft.id,
1012
1163
  draftRevision: draft.revision,
1013
1164
  draftContentHash: draft.contentHash,
1014
1165
  status: "proposed",
1015
- createdBySubjectId: normalized.createdBySubjectId,
1166
+ createdBySubjectId: input.createdBySubjectId,
1016
1167
  createdAt,
1017
1168
  })
1018
1169
  .onConflictDoNothing()
1019
1170
  .returning();
1020
1171
  if (created) {
1021
1172
  const draftRow = await getRevisionInTransaction(scopedDb, input.workspaceId, draft.id);
1022
- if (!draftRow) throw new Error("Onboarding proposal draft was not recorded");
1173
+ if (!draftRow) throw new Error("Instruction-policy proposal draft was not recorded");
1023
1174
  return onboardingProposalFromRow(created, draftRow);
1024
1175
  }
1025
1176
 
1026
1177
  const concurrentOperation = await getOnboardingProposalByOperationInTransaction(
1027
1178
  scopedDb,
1028
1179
  input.workspaceId,
1029
- normalized.operationId,
1180
+ input.operationId,
1030
1181
  );
1031
1182
  if (concurrentOperation) {
1032
- if (concurrentOperation.requestFingerprint !== normalized.requestFingerprint) {
1183
+ if (concurrentOperation.requestFingerprint !== input.requestFingerprint) {
1033
1184
  throw new WorkspaceInstructionPolicyOperationReuseError();
1034
1185
  }
1035
1186
  return await resolveOnboardingProposalInTransaction(scopedDb, concurrentOperation);
1036
1187
  }
1037
1188
  const concurrentSource = await getOnboardingProposalBySourceVersionInTransaction(
1038
1189
  scopedDb,
1039
- normalized,
1190
+ input,
1040
1191
  );
1041
1192
  if (concurrentSource) {
1042
1193
  throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
@@ -1044,8 +1195,171 @@ export async function createWorkspaceInstructionPolicyOnboardingProposal(
1044
1195
  concurrentSource.draftRevisionId,
1045
1196
  );
1046
1197
  }
1047
- throw new Error("Workspace instruction-policy onboarding proposal was not created");
1198
+ throw new Error("Workspace instruction-policy proposal was not created");
1199
+ },
1200
+ );
1201
+ }
1202
+
1203
+ export async function createWorkspaceInstructionPolicyOnboardingProposal(
1204
+ db: Database,
1205
+ input: Omit<OnboardingProposalRequestInput, "operationId"> & { operationId?: string },
1206
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
1207
+ if (input.content.trim().length === 0) {
1208
+ throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
1209
+ "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY",
1210
+ );
1211
+ }
1212
+ if (input.content.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS) {
1213
+ throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
1214
+ "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
1215
+ );
1216
+ }
1217
+ const request: OnboardingProposalRequestInput = {
1218
+ ...input,
1219
+ operationId: input.operationId ?? randomUUID(),
1220
+ sourceId: input.sourceId.trim(),
1221
+ sourceVersion: input.sourceVersion.trim(),
1222
+ };
1223
+ return await createWorkspaceInstructionPolicySourceProposal(
1224
+ db,
1225
+ { ...request, requestFingerprint: onboardingProposalRequestFingerprint(request) },
1226
+ "onboarding",
1227
+ (proposalId) => proposalId,
1228
+ );
1229
+ }
1230
+
1231
+ /**
1232
+ * Materialize an immutable Knowledge change proposal as an inactive instruction
1233
+ * draft. Human activation remains exclusively in the existing lifecycle API.
1234
+ */
1235
+ export async function createWorkspaceInstructionPolicyKnowledgeProposal(
1236
+ db: Database,
1237
+ input: WorkspaceInstructionPolicyKnowledgeProposalInput,
1238
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
1239
+ if (input.content.trim().length === 0) {
1240
+ throw new WorkspaceInstructionPolicyInvalidOperationError(
1241
+ "A Knowledge-backed instruction proposal must contain non-blank content",
1242
+ );
1243
+ }
1244
+ if (input.content.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS) {
1245
+ throw new WorkspaceInstructionPolicyInvalidOperationError(
1246
+ `A Knowledge-backed instruction proposal must not exceed ${WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters`,
1247
+ );
1248
+ }
1249
+ if (!/^[0-9a-f]{64}$/.test(input.knowledgeProposalContentHash)) {
1250
+ throw new WorkspaceInstructionPolicyInvalidOperationError(
1251
+ "Knowledge proposal content hash must be lowercase SHA-256",
1252
+ );
1253
+ }
1254
+ const request: OnboardingProposalRequestInput = {
1255
+ operationId: input.operationId,
1256
+ accountId: input.accountId,
1257
+ workspaceId: input.workspaceId,
1258
+ kind: input.kind,
1259
+ scope: input.scope,
1260
+ roleKey: input.roleKey,
1261
+ content: input.content,
1262
+ sourceId: input.knowledgeProposalId,
1263
+ sourceVersion: input.knowledgeProposalContentHash,
1264
+ confidenceBps: input.confidenceBps,
1265
+ expectedCurrentRevisionId: input.expectedCurrentRevisionId,
1266
+ expectedActivationVersion: input.expectedActivationVersion,
1267
+ createdBySubjectId: input.createdBySubjectId,
1268
+ };
1269
+ return await createWorkspaceInstructionPolicySourceProposal(
1270
+ db,
1271
+ { ...request, requestFingerprint: knowledgeProposalRequestFingerprint(request) },
1272
+ "knowledge_proposal",
1273
+ () => input.knowledgeProposalId,
1274
+ );
1275
+ }
1276
+
1277
+ /**
1278
+ * Add a successor instruction-policy proposal for a knowledge proposal whose
1279
+ * existing proposal was left behind by a moved head.
1280
+ *
1281
+ * This exists for one case: the human already answered "save", and only then
1282
+ * did the head move. Re-asking them would be the alternative. The successor
1283
+ * reuses the exact immutable source facts of the stranded proposal - same
1284
+ * knowledge proposal id, same content hash, same target, same author - so the
1285
+ * human's confirmation, which is bound to the knowledge proposal id, still
1286
+ * describes precisely what gets activated. Only the compare-and-set baseline
1287
+ * and the draft's place in the revision chain move forward.
1288
+ *
1289
+ * Returns the existing proposal unchanged when it is already current, so a
1290
+ * retry of the same confirmation is idempotent.
1291
+ */
1292
+ export async function rebaselineWorkspaceInstructionPolicyKnowledgeProposal(
1293
+ db: Database,
1294
+ input: {
1295
+ accountId: string;
1296
+ workspaceId: string;
1297
+ knowledgeProposalId: string;
1298
+ operationId: string;
1299
+ initiatingHumanSubjectId: string;
1300
+ },
1301
+ ): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
1302
+ const stranded = await withWorkspaceRls(db, input.workspaceId, async (scoped) => {
1303
+ const [row] = await scoped
1304
+ .select()
1305
+ .from(schema.workspaceInstructionPolicyOnboardingProposals)
1306
+ .where(
1307
+ and(
1308
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, input.workspaceId),
1309
+ eq(
1310
+ schema.workspaceInstructionPolicyOnboardingProposals.sourceId,
1311
+ input.knowledgeProposalId,
1312
+ ),
1313
+ eq(schema.workspaceInstructionPolicyOnboardingProposals.status, "proposed"),
1314
+ ),
1315
+ )
1316
+ .orderBy(
1317
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.baselineActivationVersion),
1318
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.createdAt),
1319
+ desc(schema.workspaceInstructionPolicyOnboardingProposals.id),
1320
+ )
1321
+ .limit(1);
1322
+ if (!row) return null;
1323
+ const draft = await getRevisionInTransaction(scoped, input.workspaceId, row.draftRevisionId);
1324
+ return draft ? { row, draft } : null;
1325
+ });
1326
+ if (!stranded) {
1327
+ throw new WorkspaceInstructionPolicyInvalidOperationError(
1328
+ "No inactive instruction-policy proposal exists for this knowledge proposal",
1329
+ );
1330
+ }
1331
+ const target: WorkspaceInstructionPolicyTarget = {
1332
+ kind: stranded.row.kind as WorkspaceInstructionPolicyKind,
1333
+ scope: stranded.row.scope as WorkspaceInstructionPolicyScope,
1334
+ roleKey: stranded.row.roleKey,
1335
+ };
1336
+ const baseline = await getWorkspaceInstructionPolicyBaseline(db, {
1337
+ workspaceId: input.workspaceId,
1338
+ target,
1339
+ });
1340
+ // The draft-validation trigger binds a knowledge-proposal draft to the
1341
+ // initiating human carried in the transaction, exactly as the original write
1342
+ // did through the governed-write authority seam. The successor must be
1343
+ // created under that same frozen human, not the ambient caller.
1344
+ return await withSessionRlsActorContext(
1345
+ {
1346
+ subjectId: input.initiatingHumanSubjectId,
1347
+ initiatingHumanSubjectId: input.initiatingHumanSubjectId,
1048
1348
  },
1349
+ async () =>
1350
+ await createWorkspaceInstructionPolicyKnowledgeProposal(db, {
1351
+ operationId: input.operationId,
1352
+ accountId: input.accountId,
1353
+ workspaceId: input.workspaceId,
1354
+ ...target,
1355
+ content: stranded.draft.content,
1356
+ knowledgeProposalId: stranded.row.sourceId,
1357
+ knowledgeProposalContentHash: stranded.row.sourceVersion,
1358
+ confidenceBps: stranded.row.confidenceBps,
1359
+ expectedCurrentRevisionId: baseline.expectedCurrentRevisionId,
1360
+ expectedActivationVersion: baseline.expectedActivationVersion,
1361
+ createdBySubjectId: stranded.row.createdBySubjectId,
1362
+ }),
1049
1363
  );
1050
1364
  }
1051
1365
 
@@ -1119,6 +1433,29 @@ function listFilterConditions(
1119
1433
  return conditions;
1120
1434
  }
1121
1435
 
1436
+ function inactiveHeadFilterConditions(
1437
+ workspaceId: string,
1438
+ query: WorkspaceInstructionPolicyListQuery,
1439
+ ): SQL[] {
1440
+ const conditions: SQL[] = [
1441
+ eq(schema.workspaceInstructionPolicyDeactivationEvents.workspaceId, workspaceId),
1442
+ ];
1443
+ if (query.kind !== undefined) {
1444
+ conditions.push(eq(schema.workspaceInstructionPolicyDeactivationEvents.kind, query.kind));
1445
+ }
1446
+ if (query.scope !== undefined) {
1447
+ conditions.push(eq(schema.workspaceInstructionPolicyDeactivationEvents.scope, query.scope));
1448
+ }
1449
+ if (query.roleKey !== undefined) {
1450
+ conditions.push(
1451
+ query.roleKey === null
1452
+ ? isNull(schema.workspaceInstructionPolicyDeactivationEvents.roleKey)
1453
+ : eq(schema.workspaceInstructionPolicyDeactivationEvents.roleKey, query.roleKey),
1454
+ );
1455
+ }
1456
+ return conditions;
1457
+ }
1458
+
1122
1459
  export async function listWorkspaceInstructionPolicyRevisions(
1123
1460
  db: Database,
1124
1461
  workspaceId: string,
@@ -1133,7 +1470,7 @@ export async function listWorkspaceInstructionPolicyRevisions(
1133
1470
  .limit(query.limit + 1);
1134
1471
  const hasMore = rows.length > query.limit;
1135
1472
  const page = rows.slice(0, query.limit);
1136
- const [heads, events] = await Promise.all([
1473
+ const [heads, inactiveHeadRows, events, deactivationEvents] = await Promise.all([
1137
1474
  scopedDb
1138
1475
  .select()
1139
1476
  .from(schema.workspaceInstructionPolicyHeads)
@@ -1143,20 +1480,82 @@ export async function listWorkspaceInstructionPolicyRevisions(
1143
1480
  asc(schema.workspaceInstructionPolicyHeads.scope),
1144
1481
  asc(schema.workspaceInstructionPolicyHeads.roleKey),
1145
1482
  ),
1483
+ scopedDb
1484
+ .selectDistinctOn([
1485
+ schema.workspaceInstructionPolicyDeactivationEvents.kind,
1486
+ schema.workspaceInstructionPolicyDeactivationEvents.scope,
1487
+ sql`coalesce(${schema.workspaceInstructionPolicyDeactivationEvents.roleKey}, '')`,
1488
+ ])
1489
+ .from(schema.workspaceInstructionPolicyDeactivationEvents)
1490
+ .where(
1491
+ and(
1492
+ ...inactiveHeadFilterConditions(workspaceId, query),
1493
+ notExists(
1494
+ scopedDb
1495
+ .select({ id: schema.workspaceInstructionPolicyHeads.id })
1496
+ .from(schema.workspaceInstructionPolicyHeads)
1497
+ .where(
1498
+ and(
1499
+ eq(
1500
+ schema.workspaceInstructionPolicyHeads.workspaceId,
1501
+ schema.workspaceInstructionPolicyDeactivationEvents.workspaceId,
1502
+ ),
1503
+ eq(
1504
+ schema.workspaceInstructionPolicyHeads.kind,
1505
+ schema.workspaceInstructionPolicyDeactivationEvents.kind,
1506
+ ),
1507
+ eq(
1508
+ schema.workspaceInstructionPolicyHeads.scope,
1509
+ schema.workspaceInstructionPolicyDeactivationEvents.scope,
1510
+ ),
1511
+ sql`${schema.workspaceInstructionPolicyHeads.roleKey} IS NOT DISTINCT FROM ${schema.workspaceInstructionPolicyDeactivationEvents.roleKey}`,
1512
+ ),
1513
+ ),
1514
+ ),
1515
+ ),
1516
+ )
1517
+ .orderBy(
1518
+ asc(schema.workspaceInstructionPolicyDeactivationEvents.kind),
1519
+ asc(schema.workspaceInstructionPolicyDeactivationEvents.scope),
1520
+ sql`coalesce(${schema.workspaceInstructionPolicyDeactivationEvents.roleKey}, '')`,
1521
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.activationVersion),
1522
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.id),
1523
+ )
1524
+ .limit(query.limit + 1),
1146
1525
  scopedDb
1147
1526
  .select()
1148
1527
  .from(schema.workspaceInstructionPolicyActivationEvents)
1149
- .where(eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId))
1528
+ .where(
1529
+ and(
1530
+ eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId),
1531
+ inArray(schema.workspaceInstructionPolicyActivationEvents.type, [
1532
+ "activate",
1533
+ "rollback",
1534
+ ]),
1535
+ ),
1536
+ )
1150
1537
  .orderBy(
1151
1538
  desc(schema.workspaceInstructionPolicyActivationEvents.createdAt),
1152
1539
  desc(schema.workspaceInstructionPolicyActivationEvents.id),
1153
1540
  )
1154
1541
  .limit(query.limit),
1542
+ scopedDb
1543
+ .select()
1544
+ .from(schema.workspaceInstructionPolicyDeactivationEvents)
1545
+ .where(eq(schema.workspaceInstructionPolicyDeactivationEvents.workspaceId, workspaceId))
1546
+ .orderBy(
1547
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.createdAt),
1548
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.id),
1549
+ )
1550
+ .limit(query.limit),
1155
1551
  ]);
1156
1552
  return {
1157
1553
  revisions: page.map(revisionFromRow),
1158
1554
  activeHeads: heads.map(headFromRow),
1555
+ inactiveHeads: inactiveHeadRows.slice(0, query.limit).map(inactiveHeadFromRow),
1556
+ inactiveHeadsTruncated: inactiveHeadRows.length > query.limit,
1159
1557
  activationEvents: events.map(eventFromRow),
1558
+ deactivationEvents: deactivationEvents.map(deactivationEventFromRow),
1160
1559
  nextAfterRevision: hasMore ? page.at(-1)!.revision : null,
1161
1560
  };
1162
1561
  });
@@ -1271,6 +1670,16 @@ async function changeActiveRevision(
1271
1670
  input.workspaceId,
1272
1671
  input.operationId,
1273
1672
  );
1673
+ if (
1674
+ !existingEvent &&
1675
+ (await hasDeactivationOperationInTransaction(
1676
+ scopedDb,
1677
+ input.workspaceId,
1678
+ input.operationId,
1679
+ ))
1680
+ ) {
1681
+ throw new WorkspaceInstructionPolicyOperationReuseError();
1682
+ }
1274
1683
  if (existingEvent) {
1275
1684
  const legacyRequestMatches =
1276
1685
  hasOwnField(input, "expectedCurrentRevisionId") &&
@@ -1303,10 +1712,15 @@ async function changeActiveRevision(
1303
1712
  };
1304
1713
  const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
1305
1714
  const currentHead = currentRow ? headFromRow(currentRow) : null;
1715
+ const inactiveBoundary = currentRow
1716
+ ? null
1717
+ : await getLatestDeactivationInTransaction(scopedDb, input.workspaceId, targetIdentity);
1718
+ const currentActivationVersion =
1719
+ currentRow?.activationVersion ?? inactiveBoundary?.activationVersion ?? 0;
1306
1720
  if (
1307
1721
  (currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
1308
1722
  (input.expectedActivationVersion !== undefined &&
1309
- (currentHead?.activationVersion ?? 0) !== input.expectedActivationVersion)
1723
+ currentActivationVersion !== input.expectedActivationVersion)
1310
1724
  ) {
1311
1725
  throw new WorkspaceInstructionPolicyConflictError(currentHead);
1312
1726
  }
@@ -1337,7 +1751,7 @@ async function changeActiveRevision(
1337
1751
  );
1338
1752
  }
1339
1753
  }
1340
- const activationVersion = (currentHead?.activationVersion ?? 0) + 1;
1754
+ const activationVersion = currentActivationVersion + 1;
1341
1755
  const createdAt = new Date();
1342
1756
  const [eventRow] = await scopedDb
1343
1757
  .insert(schema.workspaceInstructionPolicyActivationEvents)
@@ -1545,3 +1959,40 @@ function snapshotEntryMatchesRevision(
1545
1959
  revision.roleKey === entry.roleKey
1546
1960
  );
1547
1961
  }
1962
+
1963
+ /**
1964
+ * Current activation baseline for one instruction-policy target: the active
1965
+ * head's revision (or null) and the CAS activation version that a new draft
1966
+ * proposal must expect. A deactivated-to-null target keeps its monotonic
1967
+ * boundary, so the version is the max of the head and latest deactivation.
1968
+ * Read-only under workspace RLS; used by `remember` to bind a user-directed
1969
+ * rule to the exact current baseline instead of assuming an empty workspace.
1970
+ */
1971
+ export async function getWorkspaceInstructionPolicyBaseline(
1972
+ db: Database,
1973
+ input: { workspaceId: string; target: WorkspaceInstructionPolicyTarget },
1974
+ ): Promise<{ expectedCurrentRevisionId: string | null; expectedActivationVersion: number }> {
1975
+ return await withWorkspaceRls(db, input.workspaceId, async (scoped) => {
1976
+ const [head] = await scoped
1977
+ .select()
1978
+ .from(schema.workspaceInstructionPolicyHeads)
1979
+ .where(and(...headTargetConditions(input.workspaceId, input.target)))
1980
+ .limit(1);
1981
+ const [deactivation] = await scoped
1982
+ .select()
1983
+ .from(schema.workspaceInstructionPolicyDeactivationEvents)
1984
+ .where(and(...deactivationTargetConditions(input.workspaceId, input.target)))
1985
+ .orderBy(
1986
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.activationVersion),
1987
+ desc(schema.workspaceInstructionPolicyDeactivationEvents.id),
1988
+ )
1989
+ .limit(1);
1990
+ return {
1991
+ expectedCurrentRevisionId: head?.revisionId ?? null,
1992
+ expectedActivationVersion: Math.max(
1993
+ Number(head?.activationVersion ?? 0),
1994
+ Number(deactivation?.activationVersion ?? 0),
1995
+ ),
1996
+ };
1997
+ });
1998
+ }