@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.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +299 -0
- package/dist/browser-downloads.d.ts +51 -0
- package/dist/browser-identities.d.ts +137 -0
- package/dist/browser-sessions.d.ts +354 -0
- package/dist/browser-state-artifacts.d.ts +91 -0
- package/dist/canonical-human-identities.d.ts +48 -0
- package/dist/canonical-human-identities.js +28 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +194 -0
- package/dist/chunk-5KTIEDX7.js +13200 -0
- package/dist/chunk-5KTIEDX7.js.map +1 -0
- package/dist/chunk-CQZ4QSP5.js +995 -0
- package/dist/chunk-CQZ4QSP5.js.map +1 -0
- package/dist/chunk-F32YT7MP.js +117 -0
- package/dist/chunk-F32YT7MP.js.map +1 -0
- package/dist/chunk-JC5QPLUM.js +666 -0
- package/dist/chunk-JC5QPLUM.js.map +1 -0
- package/dist/chunk-P3RE4D2B.js +3535 -0
- package/dist/chunk-P3RE4D2B.js.map +1 -0
- package/dist/chunk-TAUTWOS5.js +259 -0
- package/dist/chunk-TAUTWOS5.js.map +1 -0
- package/dist/chunk-VKLB2ZGI.js +2389 -0
- package/dist/chunk-VKLB2ZGI.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +57 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +1122 -126
- package/dist/index.js +42329 -18407
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +73 -10
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-facets.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +14 -0
- package/dist/interaction-schema.d.ts +6036 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +3 -3
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +19 -6
- package/dist/schema.d.ts +22654 -9359
- package/dist/schema.js +209 -3
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +33 -7
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tenancy.d.ts +44 -0
- package/dist/session-tenancy.js +16 -0
- package/dist/session-tenancy.js.map +1 -0
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-task-policy-schema.d.ts +847 -0
- package/dist/slack-task-policy.d.ts +65 -0
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/task-notes-schema.d.ts +924 -0
- package/dist/task-notes.d.ts +85 -0
- package/dist/transcription-recordings.d.ts +2 -2
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +225 -0
- package/dist/video-generation.js +68 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/dist/xai-subscription.d.ts +326 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_browser_interaction_authority.sql +67 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_browser_download_saves.sql +34 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_browser_controller_host.sql +385 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/drizzle/0217_external_browser_auth_operations.sql +22 -0
- package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
- package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
- package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
- package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
- package/drizzle/0220_session_channels.sql +54 -0
- package/drizzle/0221_sessions_channel_index.sql +5 -0
- package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
- package/drizzle/0222_sessions_channel_fk.sql +89 -0
- package/drizzle/0223_pending_tool_event_output.sql +55 -0
- package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
- package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
- package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
- package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
- package/drizzle/0227_slack_native_actions.sql +145 -0
- package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
- package/drizzle/0228_slack_task_policy.sql +400 -0
- package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
- package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
- package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
- package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
- package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
- package/drizzle/0234_xai_subscription_authority.sql +1139 -0
- package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
- package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
- package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
- package/drizzle/0237_interaction_transition_reaper.sql +263 -0
- package/drizzle/0238_goal_persistence_policy.sql +471 -0
- package/drizzle/0238_supergrok_realtime_model.sql +29 -0
- package/drizzle/0239_supergrok_video_funding.sql +50 -0
- package/drizzle/0239_task_tree_notes.sql +990 -0
- package/package.json +26 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +4373 -0
- package/src/browser-downloads.ts +517 -0
- package/src/browser-identities.ts +1385 -0
- package/src/browser-sessions.ts +2294 -0
- package/src/browser-state-artifacts.ts +449 -0
- package/src/canonical-human-identities.ts +317 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1571 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +468 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/generated-images.ts +740 -0
- package/src/index.ts +9963 -1465
- package/src/insights.ts +251 -23
- package/src/integration-bindings.ts +455 -0
- package/src/integration-facets.ts +862 -0
- package/src/interaction-revisions.ts +70 -0
- package/src/interaction-schema.ts +1606 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +886 -0
- package/src/persistence-errors.ts +4 -7
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +534 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +721 -5
- package/src/schema.ts +6270 -3159
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +130 -42
- package/src/session-queue-commands.ts +265 -15
- package/src/session-realtime-context.ts +2 -2
- package/src/session-realtime-ledger.ts +3 -3
- package/src/session-realtime.ts +7 -7
- package/src/session-tenancy.ts +188 -0
- package/src/session-tool-call-settlement.ts +18 -3
- package/src/slack-task-policy-schema.ts +130 -0
- package/src/slack-task-policy.ts +341 -0
- package/src/slack-user-link-access.ts +702 -0
- package/src/task-notes-schema.ts +135 -0
- package/src/task-notes.ts +178 -0
- package/src/video-generation.ts +1530 -0
- package/src/workspace-artifacts.ts +1 -1
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/src/xai-subscription.ts +1887 -0
- package/dist/chunk-P6CUHXQZ.js +0 -7248
- package/dist/chunk-P6CUHXQZ.js.map +0 -1
- package/dist/chunk-VHBFHMPC.js +0 -1219
- 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
|
+
}
|