@opengeni/db 0.28.9 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,1922 @@
1
+ import {
2
+ type FileAsset,
3
+ KnowledgeSourceSyncAction,
4
+ KnowledgeSourceSyncRunSummary,
5
+ type ScheduledTask,
6
+ } from "@opengeni/contracts";
7
+ import { and, eq, inArray, sql } from "drizzle-orm";
8
+ import type { Database } from "./database";
9
+ import { setSubjectRlsContext, withRlsContext } from "./database";
10
+ import { recordKnowledgeLifecycleEvent } from "./scoped-knowledge";
11
+ import * as schema from "./schema";
12
+
13
+ export type KnowledgeSourceSyncState = {
14
+ sourceId: string;
15
+ scheduledTaskId: string;
16
+ sourceSyncGeneration: number;
17
+ sourceConfigGeneration: number;
18
+ sourceLifecycleGeneration: number;
19
+ executionCheckpoint: Record<string, unknown> | null;
20
+ executionCheckpointGeneration: number;
21
+ activeScanGeneration: number;
22
+ providerCursor: Record<string, unknown> | null;
23
+ wakeRevision: number;
24
+ pendingWakeCount: number;
25
+ bufferedWake: boolean;
26
+ bufferedScheduledTaskRunId: string | null;
27
+ reconnectRequired: boolean;
28
+ lastSuccessAt: string | null;
29
+ lastCompletedAt: string | null;
30
+ lastSummary: ReturnType<typeof KnowledgeSourceSyncRunSummary.parse> | null;
31
+ };
32
+
33
+ export type KnowledgeSourceSyncObservationFloor = {
34
+ externalObjectId: string;
35
+ providerRevision: string | null;
36
+ metadataHash: string | null;
37
+ };
38
+
39
+ export type KnowledgeSourceSyncObservationFence = {
40
+ initiatingSubjectId: string;
41
+ scanGeneration: number;
42
+ executionCheckpointGeneration: number;
43
+ observations: KnowledgeSourceSyncObservationFloor[];
44
+ };
45
+
46
+ export type KnowledgeSourceSyncObjectObservationResult = KnowledgeSourceSyncObservationFloor & {
47
+ disposition: "accepted" | "replayed" | "stale" | "conflict";
48
+ currentVersion: {
49
+ providerRevision: string | null;
50
+ metadataHash: string | null;
51
+ sourceLifecycleGeneration: number;
52
+ objectLifecycleGeneration: number;
53
+ currentObjectLifecycleGeneration: number;
54
+ objectLifecycleState: string;
55
+ aclGeneration: number;
56
+ indexObligationStatus: string | null;
57
+ } | null;
58
+ };
59
+
60
+ export function compareCanonicalDecimalProviderRevisions(
61
+ candidate: string | null,
62
+ floor: string | null,
63
+ ): -1 | 0 | 1 | null {
64
+ if (candidate === floor) return 0;
65
+ if (candidate === null || floor === null) return null;
66
+ if (!/^(0|[1-9]\d*)$/u.test(candidate) || !/^(0|[1-9]\d*)$/u.test(floor)) return null;
67
+ const candidateNumber = BigInt(candidate);
68
+ const floorNumber = BigInt(floor);
69
+ return candidateNumber < floorNumber ? -1 : candidateNumber > floorNumber ? 1 : 0;
70
+ }
71
+
72
+ export async function ensureKnowledgeSourceSyncState(
73
+ db: Database,
74
+ task: ScheduledTask,
75
+ ): Promise<KnowledgeSourceSyncState> {
76
+ const action = KnowledgeSourceSyncAction.parse(task.action);
77
+ return await withRlsContext(
78
+ db,
79
+ { accountId: task.accountId, workspaceId: task.workspaceId },
80
+ async (scopedDb) => {
81
+ const [row] = await scopedDb
82
+ .insert(schema.knowledgeSourceSyncStates)
83
+ .values({
84
+ sourceId: action.sourceId,
85
+ accountId: task.accountId,
86
+ workspaceId: task.workspaceId,
87
+ scheduledTaskId: task.id,
88
+ sourceSyncGeneration: action.sourceGeneration,
89
+ sourceLifecycleGeneration: action.sourceLifecycleGeneration,
90
+ sourceConfigGeneration: action.sourceConfigGeneration,
91
+ controlWorkspaceId: action.controlWorkspaceId,
92
+ providerCoordinationKey: action.providerCoordinationKey,
93
+ connectionId: action.connection.connectionId,
94
+ connectionVersion: action.connection.connectionVersion,
95
+ connectionProviderDomain: action.connection.providerDomain,
96
+ connectionKind: action.connection.kind,
97
+ connectionOwnerSubjectId: action.connection.ownerSubjectId,
98
+ initiatingSubjectId: action.initiatingSubjectId,
99
+ destination: action.destination,
100
+ })
101
+ .onConflictDoUpdate({
102
+ target: schema.knowledgeSourceSyncStates.sourceId,
103
+ set: {
104
+ scheduledTaskId: task.id,
105
+ sourceSyncGeneration: sql`greatest(${schema.knowledgeSourceSyncStates.sourceSyncGeneration}, ${action.sourceGeneration})`,
106
+ sourceLifecycleGeneration: sql`greatest(${schema.knowledgeSourceSyncStates.sourceLifecycleGeneration}, ${action.sourceLifecycleGeneration})`,
107
+ sourceConfigGeneration: sql`greatest(${schema.knowledgeSourceSyncStates.sourceConfigGeneration}, ${action.sourceConfigGeneration})`,
108
+ controlWorkspaceId: action.controlWorkspaceId,
109
+ providerCoordinationKey: action.providerCoordinationKey,
110
+ connectionId: action.connection.connectionId,
111
+ connectionVersion: action.connection.connectionVersion,
112
+ connectionProviderDomain: action.connection.providerDomain,
113
+ connectionKind: action.connection.kind,
114
+ connectionOwnerSubjectId: action.connection.ownerSubjectId,
115
+ initiatingSubjectId: action.initiatingSubjectId,
116
+ destination: action.destination,
117
+ updatedAt: new Date(),
118
+ },
119
+ })
120
+ .returning();
121
+ if (!row) throw new Error("Failed to persist knowledge source sync state");
122
+ return mapState(row);
123
+ },
124
+ );
125
+ }
126
+
127
+ export async function reconcileKnowledgeSourceSyncLiveGeneration(
128
+ db: Database,
129
+ input: {
130
+ accountId: string;
131
+ workspaceId: string;
132
+ sourceId: string;
133
+ scheduledTaskRunId: string;
134
+ initiatingSubjectId: string;
135
+ sourceConfigGeneration: number;
136
+ sourceLifecycleGeneration: number;
137
+ liveSourceSyncGeneration: number;
138
+ },
139
+ ): Promise<KnowledgeSourceSyncState> {
140
+ return await withRlsContext(
141
+ db,
142
+ { accountId: input.accountId, workspaceId: input.workspaceId },
143
+ async (scopedDb) =>
144
+ await scopedDb.transaction(async (tx) => {
145
+ await setSubjectRlsContext(tx, input.initiatingSubjectId);
146
+ const [state] = await tx
147
+ .select()
148
+ .from(schema.knowledgeSourceSyncStates)
149
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
150
+ .for("update")
151
+ .limit(1);
152
+ await tx.execute(sql`
153
+ SELECT knowledge_source_sync_lock_authority(
154
+ ${input.accountId}::uuid,
155
+ ${input.sourceId}::uuid,
156
+ NULL::uuid
157
+ )
158
+ `);
159
+ const [source] = await tx
160
+ .select()
161
+ .from(schema.knowledgeSources)
162
+ .where(eq(schema.knowledgeSources.id, input.sourceId))
163
+ .limit(1);
164
+ if (
165
+ !state ||
166
+ !source ||
167
+ state.leaseId !== input.scheduledTaskRunId ||
168
+ state.initiatingSubjectId !== input.initiatingSubjectId ||
169
+ state.sourceConfigGeneration !== input.sourceConfigGeneration ||
170
+ state.sourceLifecycleGeneration !== input.sourceLifecycleGeneration ||
171
+ source.lifecycleState !== "active" ||
172
+ source.lifecycleGeneration !== input.sourceLifecycleGeneration ||
173
+ source.syncGeneration !== input.liveSourceSyncGeneration ||
174
+ state.sourceSyncGeneration > source.syncGeneration
175
+ ) {
176
+ throw new Error("Knowledge source live sync generation authority changed");
177
+ }
178
+ const [updated] = await tx
179
+ .update(schema.knowledgeSourceSyncStates)
180
+ .set({ sourceSyncGeneration: source.syncGeneration, updatedAt: new Date() })
181
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
182
+ .returning();
183
+ if (!updated) throw new Error("Knowledge source sync state generation was not updated");
184
+ return mapState(updated);
185
+ }),
186
+ );
187
+ }
188
+
189
+ export async function recordKnowledgeSourceSyncWake(
190
+ db: Database,
191
+ input: {
192
+ accountId: string;
193
+ workspaceId: string;
194
+ sourceId: string;
195
+ scheduledTaskId: string;
196
+ scheduledTaskRunId: string;
197
+ cause: "scheduled" | "manual" | "initial" | "provider_event" | "retry" | "repair";
198
+ producerKey: string;
199
+ sourceConfigGeneration: number;
200
+ sourceLifecycleGeneration: number;
201
+ },
202
+ ): Promise<void> {
203
+ await withRlsContext(
204
+ db,
205
+ { accountId: input.accountId, workspaceId: input.workspaceId },
206
+ async (scopedDb) => {
207
+ await scopedDb.transaction(async (tx) => {
208
+ const [inserted] = await tx
209
+ .insert(schema.knowledgeSourceSyncWakes)
210
+ .values(input)
211
+ .onConflictDoNothing({ target: schema.knowledgeSourceSyncWakes.scheduledTaskRunId })
212
+ .returning({ id: schema.knowledgeSourceSyncWakes.id });
213
+ if (!inserted) return;
214
+ const [state] = await tx
215
+ .update(schema.knowledgeSourceSyncStates)
216
+ .set({
217
+ wakeRevision: sql`${schema.knowledgeSourceSyncStates.wakeRevision} + 1`,
218
+ pendingWakeCount: sql`${schema.knowledgeSourceSyncStates.pendingWakeCount} + 1`,
219
+ updatedAt: new Date(),
220
+ })
221
+ .where(
222
+ and(
223
+ eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId),
224
+ eq(
225
+ schema.knowledgeSourceSyncStates.sourceConfigGeneration,
226
+ input.sourceConfigGeneration,
227
+ ),
228
+ eq(
229
+ schema.knowledgeSourceSyncStates.sourceLifecycleGeneration,
230
+ input.sourceLifecycleGeneration,
231
+ ),
232
+ ),
233
+ )
234
+ .returning({ sourceId: schema.knowledgeSourceSyncStates.sourceId });
235
+ if (!state) throw new Error("Knowledge source changed before wake admission");
236
+ });
237
+ },
238
+ );
239
+ }
240
+
241
+ export async function claimKnowledgeSourceSyncLease(
242
+ db: Database,
243
+ input: {
244
+ accountId: string;
245
+ workspaceId: string;
246
+ sourceId: string;
247
+ scheduledTaskRunId: string;
248
+ overlapPolicy: "skip" | "buffer_one";
249
+ leaseMs?: number;
250
+ },
251
+ ): Promise<
252
+ | { action: "claimed"; state: KnowledgeSourceSyncState }
253
+ | { action: "skipped" | "buffered"; state: KnowledgeSourceSyncState }
254
+ > {
255
+ const leaseUntil = new Date(Date.now() + (input.leaseMs ?? 120_000));
256
+ return await withRlsContext(
257
+ db,
258
+ { accountId: input.accountId, workspaceId: input.workspaceId },
259
+ async (scopedDb) =>
260
+ await scopedDb.transaction(async (tx) => {
261
+ const [wake] = await tx
262
+ .select({ completedAt: schema.knowledgeSourceSyncWakes.completedAt })
263
+ .from(schema.knowledgeSourceSyncWakes)
264
+ .where(eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId))
265
+ .for("update")
266
+ .limit(1);
267
+ if (!wake || wake.completedAt) {
268
+ await tx
269
+ .update(schema.scheduledTaskRuns)
270
+ .set({ status: "skipped", completedAt: new Date(), updatedAt: new Date() })
271
+ .where(eq(schema.scheduledTaskRuns.id, input.scheduledTaskRunId));
272
+ const [state] = await tx
273
+ .select()
274
+ .from(schema.knowledgeSourceSyncStates)
275
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
276
+ .limit(1);
277
+ if (!state) throw new Error("Knowledge source sync state was not found");
278
+ return { action: "skipped" as const, state: mapState(state) };
279
+ }
280
+ const [state] = await tx
281
+ .select()
282
+ .from(schema.knowledgeSourceSyncStates)
283
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
284
+ .for("update")
285
+ .limit(1);
286
+ if (!state) throw new Error("Knowledge source sync state was not found");
287
+ const leaseLive = state.leaseUntil !== null && state.leaseUntil.getTime() > Date.now();
288
+ if (leaseLive && state.leaseId !== input.scheduledTaskRunId) {
289
+ if (input.overlapPolicy === "skip") {
290
+ await tx
291
+ .update(schema.scheduledTaskRuns)
292
+ .set({ status: "skipped", completedAt: new Date(), updatedAt: new Date() })
293
+ .where(eq(schema.scheduledTaskRuns.id, input.scheduledTaskRunId));
294
+ await tx
295
+ .update(schema.knowledgeSourceSyncWakes)
296
+ .set({ coalesced: true, claimedAt: new Date(), completedAt: new Date() })
297
+ .where(
298
+ eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId),
299
+ );
300
+ await tx
301
+ .update(schema.knowledgeSourceSyncStates)
302
+ .set({
303
+ pendingWakeCount: sql`greatest(${schema.knowledgeSourceSyncStates.pendingWakeCount} - 1, 0)`,
304
+ })
305
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId));
306
+ return { action: "skipped" as const, state: mapState(state) };
307
+ }
308
+ if (state.bufferedScheduledTaskRunId) {
309
+ await tx
310
+ .update(schema.scheduledTaskRuns)
311
+ .set({ status: "skipped", completedAt: new Date(), updatedAt: new Date() })
312
+ .where(eq(schema.scheduledTaskRuns.id, input.scheduledTaskRunId));
313
+ await tx
314
+ .update(schema.knowledgeSourceSyncWakes)
315
+ .set({ coalesced: true, claimedAt: new Date(), completedAt: new Date() })
316
+ .where(
317
+ eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId),
318
+ );
319
+ await tx
320
+ .update(schema.knowledgeSourceSyncStates)
321
+ .set({
322
+ pendingWakeCount: sql`greatest(${schema.knowledgeSourceSyncStates.pendingWakeCount} - 1, 0)`,
323
+ })
324
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId));
325
+ return { action: "skipped" as const, state: mapState(state) };
326
+ }
327
+ const [buffered] = await tx
328
+ .update(schema.knowledgeSourceSyncStates)
329
+ .set({
330
+ bufferedWake: true,
331
+ bufferedScheduledTaskRunId: input.scheduledTaskRunId,
332
+ updatedAt: new Date(),
333
+ })
334
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
335
+ .returning();
336
+ await tx
337
+ .update(schema.knowledgeSourceSyncWakes)
338
+ .set({ coalesced: true })
339
+ .where(
340
+ eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId),
341
+ );
342
+ return { action: "buffered" as const, state: mapState(buffered ?? state) };
343
+ }
344
+ const [claimed] = await tx
345
+ .update(schema.knowledgeSourceSyncStates)
346
+ .set({
347
+ leaseId: input.scheduledTaskRunId,
348
+ leaseUntil,
349
+ bufferedWake: false,
350
+ bufferedScheduledTaskRunId: null,
351
+ activeScanGeneration: sql`case
352
+ when ${schema.knowledgeSourceSyncStates.leaseId} = ${input.scheduledTaskRunId}::uuid
353
+ then ${schema.knowledgeSourceSyncStates.activeScanGeneration}
354
+ else ${schema.knowledgeSourceSyncStates.activeScanGeneration} + 1
355
+ end`,
356
+ updatedAt: new Date(),
357
+ })
358
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
359
+ .returning();
360
+ if (!claimed) throw new Error("Failed to claim knowledge source sync lease");
361
+ await tx
362
+ .update(schema.knowledgeSourceSyncWakes)
363
+ .set({ claimedAt: new Date() })
364
+ .where(eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId));
365
+ return { action: "claimed" as const, state: mapState(claimed) };
366
+ }),
367
+ );
368
+ }
369
+
370
+ async function assertKnowledgeSourceSyncObservationFence(
371
+ scopedDb: Database,
372
+ input: {
373
+ sourceId: string;
374
+ scheduledTaskRunId: string;
375
+ state: typeof schema.knowledgeSourceSyncStates.$inferSelect;
376
+ fence: KnowledgeSourceSyncObservationFence;
377
+ },
378
+ ): Promise<void> {
379
+ if (
380
+ input.state.leaseId !== input.scheduledTaskRunId ||
381
+ input.state.initiatingSubjectId !== input.fence.initiatingSubjectId ||
382
+ input.state.activeScanGeneration !== input.fence.scanGeneration ||
383
+ input.state.executionCheckpointGeneration !== input.fence.executionCheckpointGeneration
384
+ ) {
385
+ throw new Error("Knowledge source observation fence authority changed");
386
+ }
387
+ for (const expected of [...input.fence.observations].sort((left, right) =>
388
+ left.externalObjectId.localeCompare(right.externalObjectId),
389
+ )) {
390
+ const [observation] = await scopedDb
391
+ .select({
392
+ scheduledTaskRunId: schema.knowledgeSourceSyncObjectObservations.scheduledTaskRunId,
393
+ scanGeneration: schema.knowledgeSourceSyncObjectObservations.scanGeneration,
394
+ providerRevision: schema.knowledgeSourceSyncObjectObservations.providerRevision,
395
+ metadataHash: schema.knowledgeSourceSyncObjectObservations.metadataHash,
396
+ })
397
+ .from(schema.knowledgeSourceSyncObjectObservations)
398
+ .where(
399
+ and(
400
+ eq(schema.knowledgeSourceSyncObjectObservations.sourceId, input.sourceId),
401
+ eq(
402
+ schema.knowledgeSourceSyncObjectObservations.externalObjectId,
403
+ expected.externalObjectId,
404
+ ),
405
+ ),
406
+ )
407
+ .for("share")
408
+ .limit(1);
409
+ if (
410
+ !observation ||
411
+ observation.scheduledTaskRunId !== input.scheduledTaskRunId ||
412
+ observation.scanGeneration !== input.fence.scanGeneration ||
413
+ observation.providerRevision !== expected.providerRevision ||
414
+ observation.metadataHash !== expected.metadataHash
415
+ ) {
416
+ throw new Error("Knowledge source observation floor changed before settlement");
417
+ }
418
+ }
419
+ }
420
+
421
+ export async function checkpointKnowledgeSourceSync(
422
+ db: Database,
423
+ input: {
424
+ accountId: string;
425
+ workspaceId: string;
426
+ sourceId: string;
427
+ scheduledTaskRunId: string;
428
+ sourceConfigGeneration: number;
429
+ sourceLifecycleGeneration: number;
430
+ executionCheckpoint: Record<string, unknown>;
431
+ observationFence?: KnowledgeSourceSyncObservationFence;
432
+ },
433
+ ): Promise<KnowledgeSourceSyncState> {
434
+ return await withRlsContext(
435
+ db,
436
+ { accountId: input.accountId, workspaceId: input.workspaceId },
437
+ async (scopedDb) => {
438
+ const [state] = await scopedDb
439
+ .select()
440
+ .from(schema.knowledgeSourceSyncStates)
441
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
442
+ .for("update")
443
+ .limit(1);
444
+ if (
445
+ !state ||
446
+ state.leaseId !== input.scheduledTaskRunId ||
447
+ state.sourceConfigGeneration !== input.sourceConfigGeneration ||
448
+ state.sourceLifecycleGeneration !== input.sourceLifecycleGeneration
449
+ ) {
450
+ throw new Error("Knowledge source sync lease changed before checkpoint");
451
+ }
452
+ if (input.observationFence) {
453
+ await setSubjectRlsContext(scopedDb, input.observationFence.initiatingSubjectId);
454
+ await assertKnowledgeSourceSyncObservationFence(scopedDb, {
455
+ sourceId: input.sourceId,
456
+ scheduledTaskRunId: input.scheduledTaskRunId,
457
+ state,
458
+ fence: input.observationFence,
459
+ });
460
+ }
461
+ const [row] = await scopedDb
462
+ .update(schema.knowledgeSourceSyncStates)
463
+ .set({
464
+ executionCheckpoint: input.executionCheckpoint,
465
+ executionCheckpointGeneration: sql`${schema.knowledgeSourceSyncStates.executionCheckpointGeneration} + 1`,
466
+ leaseUntil: new Date(Date.now() + 120_000),
467
+ updatedAt: new Date(),
468
+ })
469
+ .where(
470
+ and(
471
+ eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId),
472
+ eq(schema.knowledgeSourceSyncStates.leaseId, input.scheduledTaskRunId),
473
+ eq(
474
+ schema.knowledgeSourceSyncStates.sourceConfigGeneration,
475
+ input.sourceConfigGeneration,
476
+ ),
477
+ eq(
478
+ schema.knowledgeSourceSyncStates.sourceLifecycleGeneration,
479
+ input.sourceLifecycleGeneration,
480
+ ),
481
+ ...(input.observationFence
482
+ ? [
483
+ eq(
484
+ schema.knowledgeSourceSyncStates.executionCheckpointGeneration,
485
+ input.observationFence.executionCheckpointGeneration,
486
+ ),
487
+ ]
488
+ : []),
489
+ ),
490
+ )
491
+ .returning();
492
+ if (!row) throw new Error("Knowledge source sync lease changed before checkpoint");
493
+ return mapState(row);
494
+ },
495
+ );
496
+ }
497
+
498
+ export async function settleKnowledgeSourceSyncLease(
499
+ db: Database,
500
+ input: {
501
+ accountId: string;
502
+ workspaceId: string;
503
+ sourceId: string;
504
+ scheduledTaskRunId: string;
505
+ knowledgeSyncRunId?: string | null;
506
+ status: "succeeded" | "failed";
507
+ summary: ReturnType<typeof KnowledgeSourceSyncRunSummary.parse>;
508
+ error?: string | null;
509
+ sourceConfigGeneration: number;
510
+ sourceLifecycleGeneration: number;
511
+ sourceSyncGeneration: number;
512
+ completedSourceSyncGeneration?: number | null;
513
+ executionCheckpoint?: Record<string, unknown> | null;
514
+ providerCursor?: Record<string, unknown> | null;
515
+ observationFence?: KnowledgeSourceSyncObservationFence;
516
+ },
517
+ ): Promise<{ bufferedWake: boolean; bufferedScheduledTaskRunId: string | null }> {
518
+ const summary = KnowledgeSourceSyncRunSummary.parse(input.summary);
519
+ return await withRlsContext(
520
+ db,
521
+ { accountId: input.accountId, workspaceId: input.workspaceId },
522
+ async (scopedDb) =>
523
+ await scopedDb.transaction(async (tx) => {
524
+ const [state] = await tx
525
+ .select()
526
+ .from(schema.knowledgeSourceSyncStates)
527
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
528
+ .for("update")
529
+ .limit(1);
530
+ if (
531
+ !state ||
532
+ state.leaseId !== input.scheduledTaskRunId ||
533
+ state.sourceConfigGeneration !== input.sourceConfigGeneration ||
534
+ state.sourceLifecycleGeneration !== input.sourceLifecycleGeneration
535
+ ) {
536
+ throw new Error("Knowledge source sync lease changed before settlement");
537
+ }
538
+ await setSubjectRlsContext(tx, state.initiatingSubjectId);
539
+ if (input.observationFence) {
540
+ await assertKnowledgeSourceSyncObservationFence(tx, {
541
+ sourceId: input.sourceId,
542
+ scheduledTaskRunId: input.scheduledTaskRunId,
543
+ state,
544
+ fence: input.observationFence,
545
+ });
546
+ }
547
+ await tx.execute(sql`
548
+ SELECT knowledge_source_sync_lock_authority(
549
+ ${input.accountId}::uuid,
550
+ ${input.sourceId}::uuid,
551
+ NULL::uuid
552
+ )
553
+ `);
554
+ const [source] = await tx
555
+ .select()
556
+ .from(schema.knowledgeSources)
557
+ .where(eq(schema.knowledgeSources.id, input.sourceId))
558
+ .limit(1);
559
+ const expectedSettledGeneration =
560
+ input.status === "succeeded"
561
+ ? input.completedSourceSyncGeneration
562
+ : source?.syncGeneration;
563
+ if (
564
+ !source ||
565
+ expectedSettledGeneration == null ||
566
+ source.syncGeneration !== expectedSettledGeneration ||
567
+ (input.status === "succeeded"
568
+ ? source.lifecycleState !== "active" ||
569
+ source.lifecycleGeneration !== input.sourceLifecycleGeneration ||
570
+ expectedSettledGeneration !== input.sourceSyncGeneration + 1
571
+ : source.syncGeneration < input.sourceSyncGeneration)
572
+ ) {
573
+ throw new Error("Knowledge source generation changed before lease settlement");
574
+ }
575
+ const completedAt = new Date();
576
+ await tx
577
+ .update(schema.scheduledTaskRuns)
578
+ .set({
579
+ status: input.status,
580
+ knowledgeSyncRunId: input.knowledgeSyncRunId ?? null,
581
+ knowledgeSummary: summary,
582
+ completedAt,
583
+ error: input.error ?? null,
584
+ updatedAt: completedAt,
585
+ })
586
+ .where(eq(schema.scheduledTaskRuns.id, input.scheduledTaskRunId));
587
+ await tx
588
+ .update(schema.knowledgeSourceSyncStates)
589
+ .set({
590
+ leaseId: null,
591
+ leaseUntil: null,
592
+ executionCheckpoint: input.executionCheckpoint ?? null,
593
+ ...(input.status === "succeeded" && Object.hasOwn(input, "providerCursor")
594
+ ? { providerCursor: input.providerCursor ?? null }
595
+ : {}),
596
+ bufferedWake: false,
597
+ bufferedScheduledTaskRunId: null,
598
+ reconnectRequired: summary.reconnectRequired,
599
+ sourceSyncGeneration: expectedSettledGeneration,
600
+ lastCompletedAt: completedAt,
601
+ ...(input.status === "succeeded" ? { lastSuccessAt: completedAt } : {}),
602
+ lastSummary: summary,
603
+ updatedAt: completedAt,
604
+ })
605
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId));
606
+ await tx
607
+ .update(schema.knowledgeSourceSyncWakes)
608
+ .set({ completedAt })
609
+ .where(eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId));
610
+ const [nextWake] = await tx
611
+ .select({ scheduledTaskRunId: schema.knowledgeSourceSyncWakes.scheduledTaskRunId })
612
+ .from(schema.knowledgeSourceSyncWakes)
613
+ .where(
614
+ and(
615
+ eq(schema.knowledgeSourceSyncWakes.sourceId, input.sourceId),
616
+ sql`${schema.knowledgeSourceSyncWakes.completedAt} is null`,
617
+ ),
618
+ )
619
+ .orderBy(sql`${schema.knowledgeSourceSyncWakes.createdAt} desc`)
620
+ .limit(1);
621
+ const supersededWakes = await tx
622
+ .update(schema.knowledgeSourceSyncWakes)
623
+ .set({ coalesced: true, claimedAt: completedAt, completedAt })
624
+ .where(
625
+ and(
626
+ eq(schema.knowledgeSourceSyncWakes.sourceId, input.sourceId),
627
+ sql`${schema.knowledgeSourceSyncWakes.completedAt} is null`,
628
+ ...(nextWake
629
+ ? [
630
+ sql`${schema.knowledgeSourceSyncWakes.scheduledTaskRunId} <> ${nextWake.scheduledTaskRunId}::uuid`,
631
+ ]
632
+ : []),
633
+ ),
634
+ )
635
+ .returning({ scheduledTaskRunId: schema.knowledgeSourceSyncWakes.scheduledTaskRunId });
636
+ if (supersededWakes.length > 0) {
637
+ await tx
638
+ .update(schema.scheduledTaskRuns)
639
+ .set({ status: "skipped", completedAt, updatedAt: completedAt })
640
+ .where(
641
+ sql`${schema.scheduledTaskRuns.id} in (${sql.join(
642
+ supersededWakes.map((wake) => sql`${wake.scheduledTaskRunId}::uuid`),
643
+ sql`, `,
644
+ )})`,
645
+ );
646
+ }
647
+ await tx
648
+ .update(schema.knowledgeSourceSyncStates)
649
+ .set({
650
+ pendingWakeCount: nextWake ? 1 : 0,
651
+ })
652
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId));
653
+ return {
654
+ bufferedWake: Boolean(nextWake),
655
+ bufferedScheduledTaskRunId: nextWake?.scheduledTaskRunId ?? null,
656
+ };
657
+ }),
658
+ );
659
+ }
660
+
661
+ export async function listObservedKnowledgeSourceSyncExternalObjectIds(
662
+ db: Database,
663
+ input: {
664
+ accountId: string;
665
+ workspaceId: string;
666
+ sourceId: string;
667
+ externalObjectIds: string[];
668
+ },
669
+ ): Promise<Set<string>> {
670
+ const ids = [...new Set(input.externalObjectIds)].filter((id) => id.length > 0).slice(0, 1_000);
671
+ if (ids.length === 0) return new Set();
672
+ return await withRlsContext(
673
+ db,
674
+ { accountId: input.accountId, workspaceId: input.workspaceId },
675
+ async (scopedDb) => {
676
+ const rows = await scopedDb
677
+ .select({ externalObjectId: schema.knowledgeSourceSyncObjectObservations.externalObjectId })
678
+ .from(schema.knowledgeSourceSyncObjectObservations)
679
+ .where(
680
+ and(
681
+ eq(schema.knowledgeSourceSyncObjectObservations.sourceId, input.sourceId),
682
+ inArray(schema.knowledgeSourceSyncObjectObservations.externalObjectId, ids),
683
+ ),
684
+ );
685
+ return new Set(rows.map((row) => row.externalObjectId));
686
+ },
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Release only the exact active lease after a retryable DB safety obligation
692
+ * fails. The wake and scheduled-task run deliberately remain open so Temporal
693
+ * can reclaim the same deterministic run; no provider checkpoint survives a
694
+ * reconnect/permission-loss failure.
695
+ */
696
+ export async function releaseKnowledgeSourceSyncLeaseForRetry(
697
+ db: Database,
698
+ input: {
699
+ accountId: string;
700
+ workspaceId: string;
701
+ sourceId: string;
702
+ scheduledTaskRunId: string;
703
+ },
704
+ ): Promise<void> {
705
+ await withRlsContext(
706
+ db,
707
+ { accountId: input.accountId, workspaceId: input.workspaceId },
708
+ async (scopedDb) => {
709
+ const [released] = await scopedDb
710
+ .update(schema.knowledgeSourceSyncStates)
711
+ .set({
712
+ leaseId: null,
713
+ leaseUntil: null,
714
+ executionCheckpoint: null,
715
+ updatedAt: new Date(),
716
+ })
717
+ .where(
718
+ and(
719
+ eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId),
720
+ eq(schema.knowledgeSourceSyncStates.leaseId, input.scheduledTaskRunId),
721
+ ),
722
+ )
723
+ .returning({ sourceId: schema.knowledgeSourceSyncStates.sourceId });
724
+ if (!released) {
725
+ throw new Error("Knowledge source sync lease changed before retry release");
726
+ }
727
+ },
728
+ );
729
+ }
730
+
731
+ export async function completeKnowledgeSourceSyncWakeWithoutLease(
732
+ db: Database,
733
+ input: {
734
+ accountId: string;
735
+ workspaceId: string;
736
+ sourceId: string;
737
+ scheduledTaskRunId: string;
738
+ },
739
+ ): Promise<void> {
740
+ await withRlsContext(
741
+ db,
742
+ { accountId: input.accountId, workspaceId: input.workspaceId },
743
+ async (scopedDb) => {
744
+ await scopedDb.transaction(async (tx) => {
745
+ const completedAt = new Date();
746
+ const [wake] = await tx
747
+ .update(schema.knowledgeSourceSyncWakes)
748
+ .set({ claimedAt: completedAt, completedAt })
749
+ .where(
750
+ and(
751
+ eq(schema.knowledgeSourceSyncWakes.scheduledTaskRunId, input.scheduledTaskRunId),
752
+ sql`${schema.knowledgeSourceSyncWakes.completedAt} is null`,
753
+ ),
754
+ )
755
+ .returning({ id: schema.knowledgeSourceSyncWakes.id });
756
+ if (!wake) return;
757
+ await tx
758
+ .update(schema.knowledgeSourceSyncStates)
759
+ .set({
760
+ pendingWakeCount: sql`greatest(${schema.knowledgeSourceSyncStates.pendingWakeCount} - 1, 0)`,
761
+ updatedAt: completedAt,
762
+ })
763
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId));
764
+ });
765
+ },
766
+ );
767
+ }
768
+
769
+ export async function recordKnowledgeSourceSyncItemOutcomes(
770
+ db: Database,
771
+ input: {
772
+ accountId: string;
773
+ workspaceId: string;
774
+ scheduledTaskRunId: string;
775
+ knowledgeSyncRunId?: string | null;
776
+ sourceId: string;
777
+ sourceConfigGeneration: number;
778
+ sourceLifecycleGeneration: number;
779
+ outcomes: Array<{
780
+ externalObjectId: string;
781
+ outcome: "imported" | "unchanged" | "skipped" | "failed" | "tombstoned";
782
+ reasonCode?: string | null;
783
+ detail?: string | null;
784
+ contentSha256?: string | null;
785
+ sizeBytes?: number | null;
786
+ providerRevision?: string | null;
787
+ metadataHash?: string | null;
788
+ aclEligibility?: "pending" | "eligible" | "denied";
789
+ aclEvidence?: Record<string, unknown> | null;
790
+ indexObligationId?: string | null;
791
+ }>;
792
+ },
793
+ ): Promise<void> {
794
+ if (input.outcomes.length === 0) return;
795
+ await withRlsContext(
796
+ db,
797
+ { accountId: input.accountId, workspaceId: input.workspaceId },
798
+ async (scopedDb) => {
799
+ await scopedDb
800
+ .insert(schema.knowledgeSourceSyncItemOutcomes)
801
+ .values(
802
+ input.outcomes.map((outcome) => ({
803
+ accountId: input.accountId,
804
+ workspaceId: input.workspaceId,
805
+ scheduledTaskRunId: input.scheduledTaskRunId,
806
+ knowledgeSyncRunId: input.knowledgeSyncRunId ?? null,
807
+ sourceId: input.sourceId,
808
+ sourceConfigGeneration: input.sourceConfigGeneration,
809
+ sourceLifecycleGeneration: input.sourceLifecycleGeneration,
810
+ externalObjectId: outcome.externalObjectId,
811
+ providerRevision: outcome.providerRevision ?? null,
812
+ metadataHash: outcome.metadataHash ?? null,
813
+ aclEligibility: outcome.aclEligibility ?? "pending",
814
+ aclEvidence: outcome.aclEvidence ?? null,
815
+ indexObligationId: outcome.indexObligationId ?? null,
816
+ outcome: outcome.outcome,
817
+ reasonCode: outcome.reasonCode ?? null,
818
+ detail: outcome.detail?.slice(0, 1000) ?? null,
819
+ contentSha256: outcome.contentSha256 ?? null,
820
+ sizeBytes: outcome.sizeBytes ?? null,
821
+ })),
822
+ )
823
+ .onConflictDoNothing();
824
+ },
825
+ );
826
+ }
827
+
828
+ export async function recordKnowledgeSourceSyncObjectObservations(
829
+ db: Database,
830
+ input: {
831
+ accountId: string;
832
+ workspaceId: string;
833
+ sourceId: string;
834
+ scheduledTaskRunId: string;
835
+ initiatingSubjectId: string;
836
+ scanGeneration: number;
837
+ executionCheckpointGeneration: number;
838
+ revisionOrdering?: "first_observation" | "canonical_decimal";
839
+ observations: Array<{
840
+ externalObjectId: string;
841
+ providerRevision?: string | null;
842
+ metadataHash?: string | null;
843
+ }>;
844
+ },
845
+ ): Promise<KnowledgeSourceSyncObjectObservationResult[]> {
846
+ if (input.observations.length === 0) return [];
847
+ return await withRlsContext(
848
+ db,
849
+ { accountId: input.accountId, workspaceId: input.workspaceId },
850
+ async (scopedDb) => {
851
+ await setSubjectRlsContext(scopedDb, input.initiatingSubjectId);
852
+ const [state] = await scopedDb
853
+ .select()
854
+ .from(schema.knowledgeSourceSyncStates)
855
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
856
+ .for("update")
857
+ .limit(1);
858
+ if (
859
+ !state ||
860
+ state.leaseId !== input.scheduledTaskRunId ||
861
+ state.initiatingSubjectId !== input.initiatingSubjectId ||
862
+ state.activeScanGeneration !== input.scanGeneration ||
863
+ state.executionCheckpointGeneration !== input.executionCheckpointGeneration
864
+ ) {
865
+ throw new Error("Knowledge source observation authority changed");
866
+ }
867
+
868
+ const results: KnowledgeSourceSyncObjectObservationResult[] = [];
869
+ for (const observation of input.observations) {
870
+ const candidate = {
871
+ providerRevision: observation.providerRevision ?? null,
872
+ metadataHash: observation.metadataHash ?? null,
873
+ };
874
+ const [existing] = await scopedDb
875
+ .select()
876
+ .from(schema.knowledgeSourceSyncObjectObservations)
877
+ .where(
878
+ and(
879
+ eq(schema.knowledgeSourceSyncObjectObservations.sourceId, input.sourceId),
880
+ eq(
881
+ schema.knowledgeSourceSyncObjectObservations.externalObjectId,
882
+ observation.externalObjectId,
883
+ ),
884
+ ),
885
+ )
886
+ .for("update")
887
+ .limit(1);
888
+ if (existing && existing.scanGeneration > input.scanGeneration) {
889
+ throw new Error("Knowledge source observation scan generation advanced");
890
+ }
891
+
892
+ const [currentObjectIdentity] = await scopedDb
893
+ .select({ id: schema.knowledgeSourceObjects.id })
894
+ .from(schema.knowledgeSourceObjects)
895
+ .where(
896
+ and(
897
+ eq(schema.knowledgeSourceObjects.sourceId, input.sourceId),
898
+ eq(schema.knowledgeSourceObjects.externalObjectId, observation.externalObjectId),
899
+ ),
900
+ )
901
+ .limit(1);
902
+ await scopedDb.execute(sql`
903
+ SELECT knowledge_source_sync_lock_authority(
904
+ ${input.accountId}::uuid,
905
+ ${input.sourceId}::uuid,
906
+ ${currentObjectIdentity?.id ?? null}::uuid
907
+ )
908
+ `);
909
+ // The SECURITY DEFINER helper holds the object (when present) and then
910
+ // source authority locks until this withRlsContext transaction ends.
911
+ // Re-read only after that lock so candidate classification cannot use
912
+ // pre-lock current-version metadata; plain SELECT preserves the
913
+ // intentionally read-only app-role grant on scoped-knowledge heads.
914
+ const [currentObject] = await scopedDb
915
+ .select({
916
+ id: schema.knowledgeSourceObjects.id,
917
+ currentVersionId: schema.knowledgeSourceObjects.currentVersionId,
918
+ lifecycleGeneration: schema.knowledgeSourceObjects.lifecycleGeneration,
919
+ lifecycleState: schema.knowledgeSourceObjects.lifecycleState,
920
+ })
921
+ .from(schema.knowledgeSourceObjects)
922
+ .where(
923
+ and(
924
+ eq(schema.knowledgeSourceObjects.sourceId, input.sourceId),
925
+ eq(schema.knowledgeSourceObjects.externalObjectId, observation.externalObjectId),
926
+ ),
927
+ )
928
+ .limit(1);
929
+ if (currentObjectIdentity && currentObject?.id !== currentObjectIdentity.id) {
930
+ throw new Error("Knowledge source object authority changed before observation");
931
+ }
932
+ let currentVersion: KnowledgeSourceSyncObjectObservationResult["currentVersion"] = null;
933
+ if (currentObject?.currentVersionId) {
934
+ const [current] = await scopedDb
935
+ .select({
936
+ sourceMetadata: schema.knowledgeDocumentVersions.sourceMetadata,
937
+ aclGeneration: schema.knowledgeDocumentVersions.aclGeneration,
938
+ indexObligationStatus: schema.knowledgeSourceSyncIndexObligations.status,
939
+ })
940
+ .from(schema.knowledgeDocumentVersions)
941
+ .leftJoin(
942
+ schema.knowledgeSourceSyncIndexObligations,
943
+ eq(
944
+ schema.knowledgeSourceSyncIndexObligations.knowledgeDocumentVersionId,
945
+ schema.knowledgeDocumentVersions.id,
946
+ ),
947
+ )
948
+ .where(eq(schema.knowledgeDocumentVersions.id, currentObject.currentVersionId))
949
+ .limit(1);
950
+ if (current) {
951
+ const sourceMetadata = current.sourceMetadata ?? {};
952
+ currentVersion = {
953
+ providerRevision:
954
+ typeof sourceMetadata.providerRevision === "string"
955
+ ? sourceMetadata.providerRevision
956
+ : null,
957
+ metadataHash:
958
+ typeof sourceMetadata.metadataHash === "string"
959
+ ? sourceMetadata.metadataHash
960
+ : null,
961
+ sourceLifecycleGeneration: Number(sourceMetadata.sourceLifecycleGeneration ?? 0),
962
+ objectLifecycleGeneration: Number(sourceMetadata.objectLifecycleGeneration ?? 0),
963
+ currentObjectLifecycleGeneration: currentObject.lifecycleGeneration,
964
+ objectLifecycleState: currentObject.lifecycleState,
965
+ aclGeneration: current.aclGeneration,
966
+ indexObligationStatus: current.indexObligationStatus ?? null,
967
+ };
968
+ }
969
+ }
970
+
971
+ const sameGenerationObservation =
972
+ existing?.scanGeneration === input.scanGeneration ? existing : null;
973
+ let authoritativeFloor: {
974
+ providerRevision: string | null;
975
+ metadataHash: string | null;
976
+ } | null = sameGenerationObservation
977
+ ? {
978
+ providerRevision: sameGenerationObservation.providerRevision,
979
+ metadataHash: sameGenerationObservation.metadataHash,
980
+ }
981
+ : null;
982
+ if (input.revisionOrdering === "canonical_decimal" && currentVersion) {
983
+ if (!authoritativeFloor) {
984
+ authoritativeFloor = {
985
+ providerRevision: currentVersion.providerRevision,
986
+ metadataHash: currentVersion.metadataHash,
987
+ };
988
+ } else {
989
+ const currentComparison = compareCanonicalDecimalProviderRevisions(
990
+ currentVersion.providerRevision,
991
+ authoritativeFloor.providerRevision,
992
+ );
993
+ if (
994
+ currentComparison !== null &&
995
+ (currentComparison > 0 ||
996
+ (currentComparison === 0 &&
997
+ currentVersion.metadataHash !== authoritativeFloor.metadataHash))
998
+ ) {
999
+ authoritativeFloor = {
1000
+ providerRevision: currentVersion.providerRevision,
1001
+ metadataHash: currentVersion.metadataHash,
1002
+ };
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ let disposition: KnowledgeSourceSyncObjectObservationResult["disposition"] = "accepted";
1008
+ let desiredFloor = candidate;
1009
+ if (input.revisionOrdering === "canonical_decimal" && authoritativeFloor) {
1010
+ const comparison = compareCanonicalDecimalProviderRevisions(
1011
+ candidate.providerRevision,
1012
+ authoritativeFloor.providerRevision,
1013
+ );
1014
+ if (comparison === null) {
1015
+ disposition = "conflict";
1016
+ desiredFloor = authoritativeFloor;
1017
+ } else if (comparison < 0) {
1018
+ disposition = "stale";
1019
+ desiredFloor = authoritativeFloor;
1020
+ } else if (comparison === 0) {
1021
+ disposition =
1022
+ candidate.metadataHash === authoritativeFloor.metadataHash ? "replayed" : "conflict";
1023
+ desiredFloor = authoritativeFloor;
1024
+ }
1025
+ } else if (sameGenerationObservation) {
1026
+ disposition =
1027
+ candidate.providerRevision === sameGenerationObservation.providerRevision &&
1028
+ candidate.metadataHash === sameGenerationObservation.metadataHash
1029
+ ? "replayed"
1030
+ : "stale";
1031
+ desiredFloor = {
1032
+ providerRevision: sameGenerationObservation.providerRevision,
1033
+ metadataHash: sameGenerationObservation.metadataHash,
1034
+ };
1035
+ }
1036
+
1037
+ let durable = existing;
1038
+ if (!existing) {
1039
+ [durable] = await scopedDb
1040
+ .insert(schema.knowledgeSourceSyncObjectObservations)
1041
+ .values({
1042
+ sourceId: input.sourceId,
1043
+ externalObjectId: observation.externalObjectId,
1044
+ accountId: input.accountId,
1045
+ workspaceId: input.workspaceId,
1046
+ scheduledTaskRunId: input.scheduledTaskRunId,
1047
+ scanGeneration: input.scanGeneration,
1048
+ providerRevision: desiredFloor.providerRevision,
1049
+ metadataHash: desiredFloor.metadataHash,
1050
+ observedAt: new Date(),
1051
+ })
1052
+ .onConflictDoNothing()
1053
+ .returning();
1054
+ if (!durable) throw new Error("Knowledge source observation changed before insert");
1055
+ } else if (
1056
+ existing.scanGeneration !== input.scanGeneration ||
1057
+ existing.scheduledTaskRunId !== input.scheduledTaskRunId ||
1058
+ existing.providerRevision !== desiredFloor.providerRevision ||
1059
+ existing.metadataHash !== desiredFloor.metadataHash
1060
+ ) {
1061
+ [durable] = await scopedDb
1062
+ .update(schema.knowledgeSourceSyncObjectObservations)
1063
+ .set({
1064
+ accountId: input.accountId,
1065
+ workspaceId: input.workspaceId,
1066
+ scheduledTaskRunId: input.scheduledTaskRunId,
1067
+ scanGeneration: input.scanGeneration,
1068
+ providerRevision: desiredFloor.providerRevision,
1069
+ metadataHash: desiredFloor.metadataHash,
1070
+ observedAt: new Date(),
1071
+ })
1072
+ .where(
1073
+ and(
1074
+ eq(schema.knowledgeSourceSyncObjectObservations.sourceId, input.sourceId),
1075
+ eq(
1076
+ schema.knowledgeSourceSyncObjectObservations.externalObjectId,
1077
+ observation.externalObjectId,
1078
+ ),
1079
+ eq(
1080
+ schema.knowledgeSourceSyncObjectObservations.scanGeneration,
1081
+ existing.scanGeneration,
1082
+ ),
1083
+ ),
1084
+ )
1085
+ .returning();
1086
+ if (!durable) throw new Error("Knowledge source observation floor changed");
1087
+ }
1088
+ if (!durable) throw new Error("Knowledge source observation disappeared");
1089
+ results.push({
1090
+ externalObjectId: observation.externalObjectId,
1091
+ providerRevision: durable.providerRevision,
1092
+ metadataHash: durable.metadataHash,
1093
+ disposition,
1094
+ currentVersion,
1095
+ });
1096
+ }
1097
+ return results;
1098
+ },
1099
+ );
1100
+ }
1101
+
1102
+ export async function reconcileKnowledgeSourceSyncCompleteScan(
1103
+ db: Database,
1104
+ input: {
1105
+ accountId: string;
1106
+ workspaceId: string;
1107
+ sourceId: string;
1108
+ scheduledTaskRunId: string;
1109
+ initiatingSubjectId: string;
1110
+ sourceSyncGeneration: number;
1111
+ sourceConfigGeneration: number;
1112
+ sourceLifecycleGeneration: number;
1113
+ scanGeneration: number;
1114
+ },
1115
+ ): Promise<string[]> {
1116
+ return await withRlsContext(
1117
+ db,
1118
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1119
+ async (scopedDb) =>
1120
+ await scopedDb.transaction(async (tx) => {
1121
+ await setSubjectRlsContext(tx, input.initiatingSubjectId);
1122
+ const [state] = await tx
1123
+ .select()
1124
+ .from(schema.knowledgeSourceSyncStates)
1125
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, input.sourceId))
1126
+ .for("update")
1127
+ .limit(1);
1128
+ if (
1129
+ !state ||
1130
+ state.leaseId !== input.scheduledTaskRunId ||
1131
+ state.initiatingSubjectId !== input.initiatingSubjectId ||
1132
+ state.sourceSyncGeneration !== input.sourceSyncGeneration ||
1133
+ state.sourceConfigGeneration !== input.sourceConfigGeneration ||
1134
+ state.sourceLifecycleGeneration !== input.sourceLifecycleGeneration ||
1135
+ state.activeScanGeneration !== input.scanGeneration
1136
+ ) {
1137
+ throw new Error("Knowledge source complete-scan authority changed");
1138
+ }
1139
+ const missing = await tx
1140
+ .select({
1141
+ id: schema.knowledgeSourceObjects.id,
1142
+ externalObjectId: schema.knowledgeSourceObjects.externalObjectId,
1143
+ lifecycleGeneration: schema.knowledgeSourceObjects.lifecycleGeneration,
1144
+ })
1145
+ .from(schema.knowledgeSourceObjects)
1146
+ .leftJoin(
1147
+ schema.knowledgeSourceSyncObjectObservations,
1148
+ and(
1149
+ eq(
1150
+ schema.knowledgeSourceSyncObjectObservations.sourceId,
1151
+ schema.knowledgeSourceObjects.sourceId,
1152
+ ),
1153
+ eq(
1154
+ schema.knowledgeSourceSyncObjectObservations.externalObjectId,
1155
+ schema.knowledgeSourceObjects.externalObjectId,
1156
+ ),
1157
+ ),
1158
+ )
1159
+ .where(
1160
+ and(
1161
+ eq(schema.knowledgeSourceObjects.sourceId, input.sourceId),
1162
+ eq(schema.knowledgeSourceObjects.lifecycleState, "active"),
1163
+ sql`coalesce(${schema.knowledgeSourceSyncObjectObservations.scanGeneration}, 0) < ${input.scanGeneration}`,
1164
+ ),
1165
+ )
1166
+ .orderBy(schema.knowledgeSourceObjects.id);
1167
+ if (missing[0]) {
1168
+ await tx.execute(sql`
1169
+ SELECT knowledge_source_sync_lock_authority(
1170
+ ${input.accountId}::uuid,
1171
+ ${input.sourceId}::uuid,
1172
+ ${missing[0].id}::uuid
1173
+ )
1174
+ `);
1175
+ for (const object of missing.slice(1)) {
1176
+ await tx.execute(sql`
1177
+ SELECT knowledge_source_sync_lock_authority(
1178
+ ${input.accountId}::uuid,
1179
+ ${input.sourceId}::uuid,
1180
+ ${object.id}::uuid
1181
+ )
1182
+ `);
1183
+ }
1184
+ } else {
1185
+ await tx.execute(sql`
1186
+ SELECT knowledge_source_sync_lock_authority(
1187
+ ${input.accountId}::uuid,
1188
+ ${input.sourceId}::uuid,
1189
+ NULL::uuid
1190
+ )
1191
+ `);
1192
+ }
1193
+ const [source] = await tx
1194
+ .select()
1195
+ .from(schema.knowledgeSources)
1196
+ .where(eq(schema.knowledgeSources.id, input.sourceId))
1197
+ .limit(1);
1198
+ if (
1199
+ !source ||
1200
+ source.lifecycleState !== "active" ||
1201
+ source.lifecycleGeneration !== input.sourceLifecycleGeneration ||
1202
+ source.syncGeneration !== input.sourceSyncGeneration
1203
+ ) {
1204
+ throw new Error("Knowledge source changed before complete-scan reconciliation");
1205
+ }
1206
+ for (const object of missing) {
1207
+ await recordKnowledgeLifecycleEvent(tx, {
1208
+ accountId: input.accountId,
1209
+ workspaceId: input.workspaceId,
1210
+ targetKind: "object",
1211
+ targetId: object.id,
1212
+ eventType: "deleted",
1213
+ expectedGeneration: object.lifecycleGeneration,
1214
+ operationId: `knowledge-sync-missing:${input.sourceId}:${input.scanGeneration}:${object.id}`,
1215
+ reasonCode: "authoritative_scan_absent",
1216
+ actor: {
1217
+ kind: "service",
1218
+ subjectId: "knowledge-source-sync",
1219
+ initiatingHumanSubjectId: input.initiatingSubjectId,
1220
+ },
1221
+ });
1222
+ }
1223
+ return missing.map((object) => object.externalObjectId);
1224
+ }),
1225
+ );
1226
+ }
1227
+
1228
+ export async function getKnowledgeSourceSyncIndexObligationForVersion(
1229
+ db: Database,
1230
+ input: {
1231
+ accountId: string;
1232
+ workspaceId: string;
1233
+ knowledgeDocumentVersionId: string;
1234
+ initiatingSubjectId: string;
1235
+ },
1236
+ ): Promise<{
1237
+ id: string;
1238
+ status: string;
1239
+ aclEligibility: string;
1240
+ sourceSyncGeneration: number;
1241
+ documentId: string;
1242
+ } | null> {
1243
+ return await withRlsContext(
1244
+ db,
1245
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1246
+ async (scopedDb) => {
1247
+ await setSubjectRlsContext(scopedDb, input.initiatingSubjectId);
1248
+ const [row] = await scopedDb
1249
+ .select()
1250
+ .from(schema.knowledgeSourceSyncIndexObligations)
1251
+ .where(
1252
+ eq(
1253
+ schema.knowledgeSourceSyncIndexObligations.knowledgeDocumentVersionId,
1254
+ input.knowledgeDocumentVersionId,
1255
+ ),
1256
+ )
1257
+ .limit(1);
1258
+ return row
1259
+ ? {
1260
+ id: row.id,
1261
+ status: row.status,
1262
+ aclEligibility: row.aclEligibility,
1263
+ sourceSyncGeneration: row.sourceSyncGeneration,
1264
+ documentId: row.documentId,
1265
+ }
1266
+ : null;
1267
+ },
1268
+ );
1269
+ }
1270
+
1271
+ export async function enqueueKnowledgeSourceSyncIndexObligation(
1272
+ db: Database,
1273
+ input: {
1274
+ accountId: string;
1275
+ workspaceId: string;
1276
+ scheduledTaskRunId: string;
1277
+ sourceId: string;
1278
+ sourceSyncGeneration: number;
1279
+ initiatingSubjectId: string;
1280
+ externalObjectId: string;
1281
+ knowledgeSourceObjectId: string;
1282
+ knowledgeDocumentVersionId: string;
1283
+ documentId: string;
1284
+ sourceConfigGeneration: number;
1285
+ sourceLifecycleGeneration: number;
1286
+ objectLifecycleGeneration: number;
1287
+ objectVersionGeneration: number;
1288
+ citationLocator: Record<string, unknown>;
1289
+ },
1290
+ ): Promise<{
1291
+ id: string;
1292
+ status: string;
1293
+ aclEligibility: string;
1294
+ sourceSyncGeneration: number;
1295
+ documentId: string;
1296
+ }> {
1297
+ return await withRlsContext(
1298
+ db,
1299
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1300
+ async (scopedDb) => {
1301
+ const [inserted] = await scopedDb
1302
+ .insert(schema.knowledgeSourceSyncIndexObligations)
1303
+ .values({ ...input, aclEligibility: "pending", status: "pending" })
1304
+ .onConflictDoNothing({
1305
+ target: schema.knowledgeSourceSyncIndexObligations.knowledgeDocumentVersionId,
1306
+ })
1307
+ .returning();
1308
+ const [row] = inserted
1309
+ ? [inserted]
1310
+ : await scopedDb
1311
+ .select()
1312
+ .from(schema.knowledgeSourceSyncIndexObligations)
1313
+ .where(
1314
+ eq(
1315
+ schema.knowledgeSourceSyncIndexObligations.knowledgeDocumentVersionId,
1316
+ input.knowledgeDocumentVersionId,
1317
+ ),
1318
+ )
1319
+ .limit(1);
1320
+ if (!row || row.documentId !== input.documentId || row.sourceId !== input.sourceId) {
1321
+ throw new Error("Knowledge source index obligation identity conflicted");
1322
+ }
1323
+ if (
1324
+ row.knowledgeSourceObjectId !== input.knowledgeSourceObjectId ||
1325
+ row.sourceSyncGeneration !== input.sourceSyncGeneration ||
1326
+ row.initiatingSubjectId !== input.initiatingSubjectId ||
1327
+ row.externalObjectId !== input.externalObjectId ||
1328
+ row.sourceConfigGeneration !== input.sourceConfigGeneration ||
1329
+ row.sourceLifecycleGeneration !== input.sourceLifecycleGeneration ||
1330
+ row.objectLifecycleGeneration !== input.objectLifecycleGeneration ||
1331
+ row.objectVersionGeneration !== input.objectVersionGeneration
1332
+ ) {
1333
+ throw new Error("Knowledge source index obligation authority conflicted");
1334
+ }
1335
+ return {
1336
+ id: row.id,
1337
+ status: row.status,
1338
+ aclEligibility: row.aclEligibility,
1339
+ sourceSyncGeneration: row.sourceSyncGeneration,
1340
+ documentId: row.documentId,
1341
+ };
1342
+ },
1343
+ );
1344
+ }
1345
+
1346
+ /**
1347
+ * Re-open a transiently failed index obligation only while its complete
1348
+ * source/object/version authority is still current. The immutable obligation
1349
+ * identity and original source-sync generation remain intact; the new task run
1350
+ * merely becomes the retry owner.
1351
+ */
1352
+ export async function retryKnowledgeSourceSyncIndexObligation(
1353
+ db: Database,
1354
+ input: {
1355
+ accountId: string;
1356
+ workspaceId: string;
1357
+ obligationId: string;
1358
+ scheduledTaskRunId: string;
1359
+ },
1360
+ ): Promise<"pending" | "indexed" | "invalidated"> {
1361
+ return await withRlsContext(
1362
+ db,
1363
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1364
+ async (scopedDb) =>
1365
+ await scopedDb.transaction(async (tx) => {
1366
+ const [observed] = await tx
1367
+ .select()
1368
+ .from(schema.knowledgeSourceSyncIndexObligations)
1369
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1370
+ .limit(1);
1371
+ if (!observed) throw new Error("Knowledge source index obligation was not found");
1372
+ await setSubjectRlsContext(tx, observed.initiatingSubjectId);
1373
+ const [state] = await tx
1374
+ .select()
1375
+ .from(schema.knowledgeSourceSyncStates)
1376
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, observed.sourceId))
1377
+ .for("update")
1378
+ .limit(1);
1379
+ if (state) {
1380
+ await tx.execute(sql`
1381
+ SELECT knowledge_source_sync_lock_authority(
1382
+ ${input.accountId}::uuid,
1383
+ ${observed.sourceId}::uuid,
1384
+ ${observed.knowledgeSourceObjectId}::uuid
1385
+ )
1386
+ `);
1387
+ }
1388
+ const [[obligation], [source], [object], [version]] = await Promise.all([
1389
+ tx
1390
+ .select()
1391
+ .from(schema.knowledgeSourceSyncIndexObligations)
1392
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1393
+ .for("update")
1394
+ .limit(1),
1395
+ state
1396
+ ? tx
1397
+ .select()
1398
+ .from(schema.knowledgeSources)
1399
+ .where(eq(schema.knowledgeSources.id, observed.sourceId))
1400
+ .limit(1)
1401
+ : Promise.resolve([]),
1402
+ state
1403
+ ? tx
1404
+ .select()
1405
+ .from(schema.knowledgeSourceObjects)
1406
+ .where(eq(schema.knowledgeSourceObjects.id, observed.knowledgeSourceObjectId))
1407
+ .limit(1)
1408
+ : Promise.resolve([]),
1409
+ state
1410
+ ? tx
1411
+ .select()
1412
+ .from(schema.knowledgeDocumentVersions)
1413
+ .where(eq(schema.knowledgeDocumentVersions.id, observed.knowledgeDocumentVersionId))
1414
+ .limit(1)
1415
+ : Promise.resolve([]),
1416
+ ]);
1417
+ if (!obligation) throw new Error("Knowledge source index obligation was not found");
1418
+ if (obligation.status === "indexed") return "indexed";
1419
+ if (obligation.status === "invalidated") return "invalidated";
1420
+ const current =
1421
+ state &&
1422
+ source &&
1423
+ object &&
1424
+ version &&
1425
+ state.leaseId === input.scheduledTaskRunId &&
1426
+ state.sourceSyncGeneration >= obligation.sourceSyncGeneration &&
1427
+ state.initiatingSubjectId === obligation.initiatingSubjectId &&
1428
+ state.sourceConfigGeneration === obligation.sourceConfigGeneration &&
1429
+ state.sourceLifecycleGeneration === obligation.sourceLifecycleGeneration &&
1430
+ source.lifecycleState === "active" &&
1431
+ source.syncGeneration >= obligation.sourceSyncGeneration &&
1432
+ source.lifecycleGeneration === obligation.sourceLifecycleGeneration &&
1433
+ source.currentAclGeneration === version.aclGeneration &&
1434
+ object.sourceId === obligation.sourceId &&
1435
+ object.externalObjectId === obligation.externalObjectId &&
1436
+ object.lifecycleState === "active" &&
1437
+ object.lifecycleGeneration === obligation.objectLifecycleGeneration &&
1438
+ object.versionGeneration === obligation.objectVersionGeneration &&
1439
+ object.currentVersionId === obligation.knowledgeDocumentVersionId &&
1440
+ version.objectId === obligation.knowledgeSourceObjectId &&
1441
+ version.documentId === obligation.documentId;
1442
+ if (!current) {
1443
+ const invalidatedAt = new Date();
1444
+ await tx
1445
+ .update(schema.knowledgeSourceSyncIndexObligations)
1446
+ .set({
1447
+ status: "invalidated",
1448
+ aclEligibility: "denied",
1449
+ failureCode: "stale_index_retry",
1450
+ updatedAt: invalidatedAt,
1451
+ })
1452
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId));
1453
+ await tx
1454
+ .update(schema.documents)
1455
+ .set({ agentAccess: false, chunkCount: 0, updatedAt: invalidatedAt })
1456
+ .where(eq(schema.documents.id, obligation.documentId));
1457
+ await tx
1458
+ .delete(schema.documentChunks)
1459
+ .where(eq(schema.documentChunks.documentId, obligation.documentId));
1460
+ return "invalidated";
1461
+ }
1462
+ if (obligation.status === "failed") {
1463
+ await tx
1464
+ .update(schema.knowledgeSourceSyncIndexObligations)
1465
+ .set({
1466
+ scheduledTaskRunId: input.scheduledTaskRunId,
1467
+ status: "pending",
1468
+ failureCode: null,
1469
+ updatedAt: new Date(),
1470
+ })
1471
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId));
1472
+ }
1473
+ return "pending";
1474
+ }),
1475
+ );
1476
+ }
1477
+
1478
+ export async function recordKnowledgeSourceSyncAclEvidence(
1479
+ db: Database,
1480
+ input: {
1481
+ accountId: string;
1482
+ workspaceId: string;
1483
+ obligationId: string;
1484
+ sourceSyncGeneration: number;
1485
+ sourceConfigGeneration: number;
1486
+ sourceLifecycleGeneration: number;
1487
+ objectLifecycleGeneration: number;
1488
+ objectVersionGeneration: number;
1489
+ eligibility: "eligible" | "denied";
1490
+ evidence: Record<string, unknown>;
1491
+ },
1492
+ ): Promise<void> {
1493
+ await withRlsContext(
1494
+ db,
1495
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1496
+ async (scopedDb) => {
1497
+ await scopedDb.transaction(async (tx) => {
1498
+ const [observedObligation] = await tx
1499
+ .select()
1500
+ .from(schema.knowledgeSourceSyncIndexObligations)
1501
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1502
+ .limit(1);
1503
+ if (!observedObligation) {
1504
+ throw new Error("Knowledge source ACL evidence obligation was not found");
1505
+ }
1506
+ await setSubjectRlsContext(tx, observedObligation.initiatingSubjectId);
1507
+ const [state] = await tx
1508
+ .select()
1509
+ .from(schema.knowledgeSourceSyncStates)
1510
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, observedObligation.sourceId))
1511
+ .for("update")
1512
+ .limit(1);
1513
+ if (!state) throw new Error("Knowledge source sync authorization is no longer active");
1514
+ await tx.execute(sql`
1515
+ SELECT knowledge_source_sync_lock_authority(
1516
+ ${input.accountId}::uuid,
1517
+ ${observedObligation.sourceId}::uuid,
1518
+ ${observedObligation.knowledgeSourceObjectId}::uuid
1519
+ )
1520
+ `);
1521
+ const [source] = await tx
1522
+ .select()
1523
+ .from(schema.knowledgeSources)
1524
+ .where(eq(schema.knowledgeSources.id, observedObligation.sourceId))
1525
+ .limit(1);
1526
+ const [object] = await tx
1527
+ .select()
1528
+ .from(schema.knowledgeSourceObjects)
1529
+ .where(eq(schema.knowledgeSourceObjects.id, observedObligation.knowledgeSourceObjectId))
1530
+ .limit(1);
1531
+ const [version] = await tx
1532
+ .select()
1533
+ .from(schema.knowledgeDocumentVersions)
1534
+ .where(
1535
+ eq(schema.knowledgeDocumentVersions.id, observedObligation.knowledgeDocumentVersionId),
1536
+ )
1537
+ .limit(1);
1538
+ const [obligation] = await tx
1539
+ .select()
1540
+ .from(schema.knowledgeSourceSyncIndexObligations)
1541
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1542
+ .for("update")
1543
+ .limit(1);
1544
+ const [document] = obligation
1545
+ ? await tx
1546
+ .select()
1547
+ .from(schema.documents)
1548
+ .where(eq(schema.documents.id, obligation.documentId))
1549
+ .for("update")
1550
+ .limit(1)
1551
+ : [];
1552
+ if (
1553
+ !obligation ||
1554
+ !source ||
1555
+ !object ||
1556
+ !version ||
1557
+ !document ||
1558
+ input.sourceSyncGeneration !== obligation.sourceSyncGeneration ||
1559
+ state.sourceSyncGeneration < input.sourceSyncGeneration ||
1560
+ state.initiatingSubjectId !== obligation.initiatingSubjectId ||
1561
+ state.sourceSyncGeneration < obligation.sourceSyncGeneration ||
1562
+ state.sourceConfigGeneration !== input.sourceConfigGeneration ||
1563
+ state.sourceConfigGeneration !== obligation.sourceConfigGeneration ||
1564
+ state.sourceLifecycleGeneration !== input.sourceLifecycleGeneration ||
1565
+ state.sourceLifecycleGeneration !== obligation.sourceLifecycleGeneration ||
1566
+ source.lifecycleState !== "active" ||
1567
+ source.syncGeneration < obligation.sourceSyncGeneration ||
1568
+ source.lifecycleGeneration !== obligation.sourceLifecycleGeneration ||
1569
+ source.currentAclGeneration !== version.aclGeneration ||
1570
+ object.sourceId !== obligation.sourceId ||
1571
+ object.externalObjectId !== obligation.externalObjectId ||
1572
+ object.lifecycleState !== "active" ||
1573
+ object.lifecycleGeneration !== obligation.objectLifecycleGeneration ||
1574
+ object.versionGeneration !== obligation.objectVersionGeneration ||
1575
+ object.currentVersionId !== obligation.knowledgeDocumentVersionId ||
1576
+ version.sourceId !== obligation.sourceId ||
1577
+ version.objectId !== obligation.knowledgeSourceObjectId ||
1578
+ version.versionGeneration !== obligation.objectVersionGeneration ||
1579
+ version.documentId !== obligation.documentId ||
1580
+ version.fileId !== document.fileId ||
1581
+ document.accountId !== input.accountId ||
1582
+ document.workspaceId !== input.workspaceId ||
1583
+ document.sourceExternalId !== obligation.externalObjectId ||
1584
+ !document.knowledgeSourceIdentity ||
1585
+ obligation.sourceConfigGeneration !== input.sourceConfigGeneration ||
1586
+ obligation.sourceSyncGeneration !== input.sourceSyncGeneration ||
1587
+ obligation.sourceLifecycleGeneration !== input.sourceLifecycleGeneration ||
1588
+ obligation.objectLifecycleGeneration !== input.objectLifecycleGeneration ||
1589
+ obligation.objectVersionGeneration !== input.objectVersionGeneration
1590
+ ) {
1591
+ throw new Error("Knowledge source ACL evidence authority changed");
1592
+ }
1593
+ if (input.eligibility === "eligible" && obligation.status !== "indexed") {
1594
+ throw new Error("Knowledge source ACL eligibility requires a completed index obligation");
1595
+ }
1596
+ await tx
1597
+ .update(schema.knowledgeSourceSyncIndexObligations)
1598
+ .set({ aclEligibility: input.eligibility, updatedAt: new Date() })
1599
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId));
1600
+ await tx
1601
+ .update(schema.knowledgeSourceSyncItemOutcomes)
1602
+ .set({ aclEligibility: input.eligibility, aclEvidence: input.evidence })
1603
+ .where(eq(schema.knowledgeSourceSyncItemOutcomes.indexObligationId, input.obligationId));
1604
+ const [updatedDocument] = await tx
1605
+ .update(schema.documents)
1606
+ .set({ agentAccess: input.eligibility === "eligible", updatedAt: new Date() })
1607
+ .where(eq(schema.documents.id, obligation.documentId))
1608
+ .returning({ id: schema.documents.id });
1609
+ if (!updatedDocument) {
1610
+ throw new Error("Knowledge source ACL evidence document authority changed");
1611
+ }
1612
+ });
1613
+ },
1614
+ );
1615
+ }
1616
+
1617
+ /** Bounded retention hook. Callers choose policy and cadence; pending index or
1618
+ * ACL obligations are never deleted by age alone. */
1619
+ export async function pruneKnowledgeSourceSyncEvidence(
1620
+ db: Database,
1621
+ input: {
1622
+ accountId: string;
1623
+ workspaceId: string;
1624
+ before: Date;
1625
+ limit?: number;
1626
+ },
1627
+ ): Promise<{ itemOutcomes: number; wakes: number; indexObligations: number }> {
1628
+ const limit = Math.max(1, Math.min(input.limit ?? 500, 5_000));
1629
+ return await withRlsContext(
1630
+ db,
1631
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1632
+ async (scopedDb) => {
1633
+ const [counts] = await scopedDb.execute<{
1634
+ item_outcomes: number;
1635
+ wakes: number;
1636
+ index_obligations: number;
1637
+ }>(sql`
1638
+ WITH deleted_items AS (
1639
+ DELETE FROM knowledge_source_sync_item_outcomes
1640
+ WHERE id IN (
1641
+ SELECT id FROM knowledge_source_sync_item_outcomes
1642
+ WHERE workspace_id = ${input.workspaceId}::uuid AND created_at < ${input.before}
1643
+ ORDER BY created_at ASC LIMIT ${limit}
1644
+ ) RETURNING 1
1645
+ ), deleted_wakes AS (
1646
+ DELETE FROM knowledge_source_sync_wakes
1647
+ WHERE id IN (
1648
+ SELECT id FROM knowledge_source_sync_wakes
1649
+ WHERE workspace_id = ${input.workspaceId}::uuid
1650
+ AND completed_at IS NOT NULL AND completed_at < ${input.before}
1651
+ ORDER BY completed_at ASC LIMIT ${limit}
1652
+ ) RETURNING 1
1653
+ ), deleted_obligations AS (
1654
+ DELETE FROM knowledge_source_sync_index_obligations
1655
+ WHERE id IN (
1656
+ SELECT id FROM knowledge_source_sync_index_obligations
1657
+ WHERE workspace_id = ${input.workspaceId}::uuid
1658
+ AND status IN ('indexed', 'failed', 'invalidated')
1659
+ AND acl_eligibility <> 'pending'
1660
+ AND updated_at < ${input.before}
1661
+ ORDER BY updated_at ASC LIMIT ${limit}
1662
+ ) RETURNING 1
1663
+ )
1664
+ SELECT
1665
+ (SELECT count(*)::int FROM deleted_items) AS item_outcomes,
1666
+ (SELECT count(*)::int FROM deleted_wakes) AS wakes,
1667
+ (SELECT count(*)::int FROM deleted_obligations) AS index_obligations
1668
+ `);
1669
+ return {
1670
+ itemOutcomes: counts?.item_outcomes ?? 0,
1671
+ wakes: counts?.wakes ?? 0,
1672
+ indexObligations: counts?.index_obligations ?? 0,
1673
+ };
1674
+ },
1675
+ );
1676
+ }
1677
+
1678
+ export async function settleKnowledgeSourceSyncIndexObligation(
1679
+ db: Database,
1680
+ input: {
1681
+ accountId: string;
1682
+ workspaceId: string;
1683
+ obligationId: string;
1684
+ status: "indexed" | "failed" | "invalidated";
1685
+ failureCode?: string | null;
1686
+ },
1687
+ ): Promise<"settled" | "invalidated"> {
1688
+ return await withRlsContext(
1689
+ db,
1690
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1691
+ async (scopedDb) => {
1692
+ const settlement = await scopedDb.transaction(async (tx) => {
1693
+ const [observedObligation] = await tx
1694
+ .select()
1695
+ .from(schema.knowledgeSourceSyncIndexObligations)
1696
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1697
+ .limit(1);
1698
+ if (!observedObligation) {
1699
+ throw new Error("Knowledge source index obligation is no longer pending");
1700
+ }
1701
+ await setSubjectRlsContext(tx, observedObligation.initiatingSubjectId);
1702
+ const [state] = await tx
1703
+ .select()
1704
+ .from(schema.knowledgeSourceSyncStates)
1705
+ .where(eq(schema.knowledgeSourceSyncStates.sourceId, observedObligation.sourceId))
1706
+ .for("update")
1707
+ .limit(1);
1708
+ if (state) {
1709
+ await tx.execute(sql`
1710
+ SELECT knowledge_source_sync_lock_authority(
1711
+ ${input.accountId}::uuid,
1712
+ ${observedObligation.sourceId}::uuid,
1713
+ ${observedObligation.knowledgeSourceObjectId}::uuid
1714
+ )
1715
+ `);
1716
+ }
1717
+ const [obligation] = await tx
1718
+ .select()
1719
+ .from(schema.knowledgeSourceSyncIndexObligations)
1720
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId))
1721
+ .for("update")
1722
+ .limit(1);
1723
+ if (!obligation) {
1724
+ throw new Error("Knowledge source index obligation is no longer pending");
1725
+ }
1726
+ if (
1727
+ obligation.sourceId !== observedObligation.sourceId ||
1728
+ obligation.knowledgeSourceObjectId !== observedObligation.knowledgeSourceObjectId ||
1729
+ obligation.knowledgeDocumentVersionId !== observedObligation.knowledgeDocumentVersionId ||
1730
+ obligation.documentId !== observedObligation.documentId ||
1731
+ obligation.initiatingSubjectId !== observedObligation.initiatingSubjectId
1732
+ ) {
1733
+ throw new Error("Knowledge source index obligation authority changed");
1734
+ }
1735
+ const cleanInvalidatedDocument = async () => {
1736
+ const invalidatedAt = new Date();
1737
+ await tx
1738
+ .update(schema.documents)
1739
+ .set({ agentAccess: false, chunkCount: 0, updatedAt: invalidatedAt })
1740
+ .where(eq(schema.documents.id, obligation.documentId));
1741
+ await tx
1742
+ .delete(schema.documentChunks)
1743
+ .where(eq(schema.documentChunks.documentId, obligation.documentId));
1744
+ };
1745
+ if (obligation.status !== "pending") {
1746
+ if (input.status === "indexed" && obligation.status === "invalidated") {
1747
+ await cleanInvalidatedDocument();
1748
+ return "invalidated" as const;
1749
+ }
1750
+ throw new Error("Knowledge source index obligation is no longer pending");
1751
+ }
1752
+ if (input.status === "indexed") {
1753
+ const [source] = state
1754
+ ? await tx
1755
+ .select()
1756
+ .from(schema.knowledgeSources)
1757
+ .where(eq(schema.knowledgeSources.id, obligation.sourceId))
1758
+ .limit(1)
1759
+ : [];
1760
+ const [object] = state
1761
+ ? await tx
1762
+ .select()
1763
+ .from(schema.knowledgeSourceObjects)
1764
+ .where(eq(schema.knowledgeSourceObjects.id, obligation.knowledgeSourceObjectId))
1765
+ .limit(1)
1766
+ : [];
1767
+ const [version] = state
1768
+ ? await tx
1769
+ .select()
1770
+ .from(schema.knowledgeDocumentVersions)
1771
+ .where(
1772
+ eq(schema.knowledgeDocumentVersions.id, obligation.knowledgeDocumentVersionId),
1773
+ )
1774
+ .limit(1)
1775
+ : [];
1776
+ const current =
1777
+ state &&
1778
+ source &&
1779
+ object &&
1780
+ version &&
1781
+ state.sourceSyncGeneration >= obligation.sourceSyncGeneration &&
1782
+ state.initiatingSubjectId === obligation.initiatingSubjectId &&
1783
+ state.sourceConfigGeneration === obligation.sourceConfigGeneration &&
1784
+ state.sourceLifecycleGeneration === obligation.sourceLifecycleGeneration &&
1785
+ source.lifecycleState === "active" &&
1786
+ source.syncGeneration >= obligation.sourceSyncGeneration &&
1787
+ source.lifecycleGeneration === obligation.sourceLifecycleGeneration &&
1788
+ source.currentAclGeneration === version.aclGeneration &&
1789
+ object.sourceId === obligation.sourceId &&
1790
+ object.externalObjectId === obligation.externalObjectId &&
1791
+ object.lifecycleState === "active" &&
1792
+ object.lifecycleGeneration === obligation.objectLifecycleGeneration &&
1793
+ object.versionGeneration === obligation.objectVersionGeneration &&
1794
+ object.currentVersionId === obligation.knowledgeDocumentVersionId &&
1795
+ version.objectId === obligation.knowledgeSourceObjectId &&
1796
+ version.documentId === obligation.documentId;
1797
+ if (!current) {
1798
+ const invalidatedAt = new Date();
1799
+ await tx
1800
+ .update(schema.knowledgeSourceSyncIndexObligations)
1801
+ .set({
1802
+ status: "invalidated",
1803
+ aclEligibility: "denied",
1804
+ failureCode: "stale_index_completion",
1805
+ updatedAt: invalidatedAt,
1806
+ })
1807
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId));
1808
+ await cleanInvalidatedDocument();
1809
+ return "invalidated" as const;
1810
+ }
1811
+ }
1812
+ await tx
1813
+ .update(schema.knowledgeSourceSyncIndexObligations)
1814
+ .set({
1815
+ status: input.status,
1816
+ failureCode: input.failureCode ?? null,
1817
+ updatedAt: new Date(),
1818
+ })
1819
+ .where(eq(schema.knowledgeSourceSyncIndexObligations.id, input.obligationId));
1820
+ return "settled" as const;
1821
+ });
1822
+ return settlement;
1823
+ },
1824
+ );
1825
+ }
1826
+
1827
+ export async function ensureKnowledgeSourceBlobFile(
1828
+ db: Database,
1829
+ input: {
1830
+ accountId: string;
1831
+ workspaceId: string;
1832
+ fileId: string;
1833
+ filename: string;
1834
+ safeFilename: string;
1835
+ contentType: string;
1836
+ sizeBytes: number;
1837
+ sha256: string;
1838
+ bucket: string;
1839
+ objectKey: string;
1840
+ },
1841
+ ): Promise<FileAsset> {
1842
+ return await withRlsContext(
1843
+ db,
1844
+ { accountId: input.accountId, workspaceId: input.workspaceId },
1845
+ async (scopedDb) => {
1846
+ const [inserted] = await scopedDb
1847
+ .insert(schema.files)
1848
+ .values({
1849
+ id: input.fileId,
1850
+ accountId: input.accountId,
1851
+ workspaceId: input.workspaceId,
1852
+ status: "ready",
1853
+ filename: input.filename,
1854
+ safeFilename: input.safeFilename,
1855
+ contentType: input.contentType,
1856
+ sizeBytes: input.sizeBytes,
1857
+ sha256: input.sha256,
1858
+ bucket: input.bucket,
1859
+ objectKey: input.objectKey,
1860
+ })
1861
+ .onConflictDoNothing({ target: schema.files.objectKey })
1862
+ .returning();
1863
+ const [row] = inserted
1864
+ ? [inserted]
1865
+ : await scopedDb
1866
+ .select()
1867
+ .from(schema.files)
1868
+ .where(eq(schema.files.objectKey, input.objectKey))
1869
+ .limit(1);
1870
+ if (
1871
+ !row ||
1872
+ row.accountId !== input.accountId ||
1873
+ row.workspaceId !== input.workspaceId ||
1874
+ row.status !== "ready" ||
1875
+ row.sha256 !== input.sha256 ||
1876
+ row.sizeBytes !== input.sizeBytes ||
1877
+ row.contentType !== input.contentType
1878
+ ) {
1879
+ throw new Error("Canonical knowledge blob identity conflicted");
1880
+ }
1881
+ return {
1882
+ id: row.id,
1883
+ accountId: row.accountId,
1884
+ workspaceId: row.workspaceId,
1885
+ status: row.status as FileAsset["status"],
1886
+ filename: row.filename,
1887
+ safeFilename: row.safeFilename,
1888
+ contentType: row.contentType,
1889
+ sizeBytes: row.sizeBytes,
1890
+ sha256: row.sha256,
1891
+ bucket: row.bucket,
1892
+ objectKey: row.objectKey,
1893
+ createdAt: row.createdAt.toISOString(),
1894
+ updatedAt: row.updatedAt.toISOString(),
1895
+ };
1896
+ },
1897
+ );
1898
+ }
1899
+
1900
+ function mapState(
1901
+ row: typeof schema.knowledgeSourceSyncStates.$inferSelect,
1902
+ ): KnowledgeSourceSyncState {
1903
+ return {
1904
+ sourceId: row.sourceId,
1905
+ scheduledTaskId: row.scheduledTaskId,
1906
+ sourceSyncGeneration: row.sourceSyncGeneration,
1907
+ sourceConfigGeneration: row.sourceConfigGeneration,
1908
+ sourceLifecycleGeneration: row.sourceLifecycleGeneration,
1909
+ executionCheckpoint: row.executionCheckpoint,
1910
+ executionCheckpointGeneration: row.executionCheckpointGeneration,
1911
+ activeScanGeneration: row.activeScanGeneration,
1912
+ providerCursor: row.providerCursor,
1913
+ wakeRevision: row.wakeRevision,
1914
+ pendingWakeCount: row.pendingWakeCount,
1915
+ bufferedWake: row.bufferedWake,
1916
+ bufferedScheduledTaskRunId: row.bufferedScheduledTaskRunId,
1917
+ reconnectRequired: row.reconnectRequired,
1918
+ lastSuccessAt: row.lastSuccessAt?.toISOString() ?? null,
1919
+ lastCompletedAt: row.lastCompletedAt?.toISOString() ?? null,
1920
+ lastSummary: row.lastSummary ? KnowledgeSourceSyncRunSummary.parse(row.lastSummary) : null,
1921
+ };
1922
+ }