@opengeni/documents 0.5.41 → 0.7.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,21 @@
1
1
  // src/index.ts
2
2
  import {
3
- KnowledgeProviderCitation
3
+ KnowledgeProviderCitation,
4
+ DocumentAuthorityReclassification,
5
+ ListDocumentAuthorityReclassificationsResponse,
6
+ DocumentDefaultCollectionBackfill,
7
+ DocumentDefaultCollectionBackfillAudit,
8
+ DocumentDefaultCollectionBackfillOperationAudit,
9
+ DocumentDefaultCollectionBackfillReceiptAudit,
10
+ DocumentDefaultCollectionBackfillRunAudit,
11
+ ListDocumentDefaultCollectionBackfillRunsResponse,
12
+ ListOrganizationDocumentAuthorityReclassificationsResponse,
13
+ OrganizationDocumentAuthorityReclassification
4
14
  } from "@opengeni/contracts";
5
15
  import {
16
+ createPersonalDocumentAuthority,
6
17
  getFilesForSubject,
18
+ resolveDocumentOriginalFileForSubject,
7
19
  rlsContextForWorkspace,
8
20
  setSubjectRlsContext,
9
21
  withRlsContext,
@@ -11,9 +23,20 @@ import {
11
23
  withWorkspaceSubjectRls
12
24
  } from "@opengeni/db";
13
25
  import * as schema from "@opengeni/db/schema";
14
- import { createHash } from "crypto";
15
- import { and, asc, desc, eq, gt, inArray, isNotNull, or, sql } from "drizzle-orm";
16
- import { KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS } from "@opengeni/contracts";
26
+ import { retryWhileMissing } from "@opengeni/storage";
27
+ import { createHash, randomUUID } from "crypto";
28
+ import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
29
+ import {
30
+ KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS,
31
+ KNOWLEDGE_BROWSE_MAX_LIMIT,
32
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
33
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
34
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS,
35
+ KNOWLEDGE_SEARCH_MAX_RESULTS,
36
+ KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE,
37
+ KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
38
+ KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
39
+ } from "@opengeni/contracts";
17
40
 
18
41
  // src/knowledge-projection.ts
19
42
  import {
@@ -697,6 +720,324 @@ async function listDocumentBasesEnsuringDefault(db, input) {
697
720
  await ensureDefaultBase(db, input);
698
721
  return await listDocumentBases(db, input.workspaceId);
699
722
  }
723
+ async function runDocumentDefaultCollectionBackfill(db, input) {
724
+ return await withDocumentAccountRls(
725
+ db,
726
+ input.accountId,
727
+ input.workspaceId,
728
+ { viewerSubjectId: input.actorSubjectId },
729
+ async (scopedDb) => {
730
+ const command = {
731
+ accountId: input.accountId,
732
+ workspaceId: input.workspaceId,
733
+ actorSubjectId: input.actorSubjectId,
734
+ runId: input.runId,
735
+ operationId: input.operationId,
736
+ batchSize: input.batchSize,
737
+ accountAdminAuthorization: input.accountAdminAuthorization
738
+ };
739
+ const rows = await scopedDb.execute(sql`
740
+ SELECT run_document_default_collection_backfill(
741
+ ${JSON.stringify(command)}::jsonb
742
+ ) AS result
743
+ `);
744
+ const row = rows[0];
745
+ if (!row) throw new Error("Document Default collection backfill returned no result");
746
+ return DocumentDefaultCollectionBackfill.parse(row.result);
747
+ }
748
+ );
749
+ }
750
+ async function listDocumentDefaultCollectionBackfillRuns(db, input) {
751
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
752
+ const cursor = input.cursor ? decodeDocumentMigrationAuditCursor(input.cursor, ["backfill-runs", input.accountId], true) : null;
753
+ return await withDocumentAccountRls(
754
+ db,
755
+ input.accountId,
756
+ input.workspaceId,
757
+ { viewerSubjectId: input.actorSubjectId },
758
+ async (scopedDb) => {
759
+ const command = {
760
+ accountId: input.accountId,
761
+ workspaceId: input.workspaceId,
762
+ actorSubjectId: input.actorSubjectId,
763
+ accountAdminAuthorization: input.accountAdminAuthorization,
764
+ limit: limit + 1,
765
+ beforeStartedAt: cursor?.timestamp ?? null,
766
+ beforeRunId: cursor?.id ?? null
767
+ };
768
+ const rows = await scopedDb.execute(sql`
769
+ SELECT list_document_default_collection_backfill_runs(
770
+ ${JSON.stringify(command)}::jsonb
771
+ ) AS result
772
+ `);
773
+ const parsed = rows.map((row) => DocumentDefaultCollectionBackfillRunAudit.parse(row.result));
774
+ const hasMore = parsed.length > limit;
775
+ const runs = parsed.slice(0, limit);
776
+ const tail = hasMore ? runs.at(-1) : null;
777
+ return ListDocumentDefaultCollectionBackfillRunsResponse.parse({
778
+ runs,
779
+ hasMore,
780
+ nextCursor: tail ? encodeDocumentMigrationAuditCursor(["backfill-runs", input.accountId], {
781
+ timestamp: tail.startedAt,
782
+ id: tail.runId
783
+ }) : null
784
+ });
785
+ }
786
+ );
787
+ }
788
+ async function getDocumentDefaultCollectionBackfillAudit(db, input) {
789
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
790
+ const operationCursor = input.operationCursor ? decodeDocumentMigrationAuditCursor(
791
+ input.operationCursor,
792
+ ["backfill-operations", input.accountId, input.runId],
793
+ true
794
+ ) : null;
795
+ const receiptCursor = input.receiptCursor ? decodeDocumentMigrationAuditCursor(
796
+ input.receiptCursor,
797
+ ["backfill-receipts", input.accountId, input.runId],
798
+ false
799
+ ) : null;
800
+ return await withDocumentAccountRls(
801
+ db,
802
+ input.accountId,
803
+ input.workspaceId,
804
+ { viewerSubjectId: input.actorSubjectId },
805
+ async (scopedDb) => {
806
+ const command = {
807
+ accountId: input.accountId,
808
+ workspaceId: input.workspaceId,
809
+ actorSubjectId: input.actorSubjectId,
810
+ accountAdminAuthorization: input.accountAdminAuthorization,
811
+ runId: input.runId,
812
+ limit,
813
+ operationBeforeCreatedAt: operationCursor?.timestamp ?? null,
814
+ operationBeforeId: operationCursor?.id ?? null,
815
+ receiptAfterWorkspaceId: receiptCursor?.id ?? null
816
+ };
817
+ const rows = await scopedDb.execute(sql`
818
+ SELECT get_document_default_collection_backfill_audit(
819
+ ${JSON.stringify(command)}::jsonb
820
+ ) AS result
821
+ `);
822
+ const raw = rows[0]?.result;
823
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
824
+ throw new Error("document Default collection backfill audit returned no result");
825
+ }
826
+ const value = raw;
827
+ const run = DocumentDefaultCollectionBackfillRunAudit.parse(value.run);
828
+ const operations = Array.isArray(value.operations) ? value.operations.map(
829
+ (operation) => DocumentDefaultCollectionBackfillOperationAudit.parse(operation)
830
+ ) : [];
831
+ const receipts = Array.isArray(value.receipts) ? value.receipts.map(
832
+ (receipt) => DocumentDefaultCollectionBackfillReceiptAudit.parse(receipt)
833
+ ) : [];
834
+ const operationsHasMore = operations.length > limit;
835
+ const receiptsHasMore = receipts.length > limit;
836
+ const operationPage = operations.slice(0, limit);
837
+ const receiptPage = receipts.slice(0, limit);
838
+ const operationTail = operationsHasMore ? operationPage.at(-1) : null;
839
+ const receiptTail = receiptsHasMore ? receiptPage.at(-1) : null;
840
+ return DocumentDefaultCollectionBackfillAudit.parse({
841
+ run,
842
+ operations: operationPage,
843
+ receipts: receiptPage,
844
+ operationsHasMore,
845
+ operationsNextCursor: operationTail ? encodeDocumentMigrationAuditCursor(
846
+ ["backfill-operations", input.accountId, input.runId],
847
+ { timestamp: operationTail.createdAt, id: operationTail.operationId }
848
+ ) : null,
849
+ receiptsHasMore,
850
+ receiptsNextCursor: receiptTail ? encodeDocumentMigrationAuditCursor(
851
+ ["backfill-receipts", input.accountId, input.runId],
852
+ { timestamp: null, id: receiptTail.workspaceId }
853
+ ) : null
854
+ });
855
+ }
856
+ );
857
+ }
858
+ async function listOrganizationDocumentAuthorityReclassifications(db, input) {
859
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
860
+ const cursor = input.cursor ? decodeDocumentMigrationAuditCursor(
861
+ input.cursor,
862
+ ["organization-reclassifications", input.accountId],
863
+ true
864
+ ) : null;
865
+ return await withDocumentAccountRls(
866
+ db,
867
+ input.accountId,
868
+ input.workspaceId,
869
+ { viewerSubjectId: input.actorSubjectId },
870
+ async (scopedDb) => {
871
+ const command = {
872
+ accountId: input.accountId,
873
+ workspaceId: input.workspaceId,
874
+ actorSubjectId: input.actorSubjectId,
875
+ accountAdminAuthorization: input.accountAdminAuthorization,
876
+ limit: limit + 1,
877
+ beforeCreatedAt: cursor?.timestamp ?? null,
878
+ beforeOperationId: cursor?.id ?? null
879
+ };
880
+ const rows = await scopedDb.execute(sql`
881
+ SELECT list_organization_document_authority_reclassifications(
882
+ ${JSON.stringify(command)}::jsonb
883
+ ) AS result
884
+ `);
885
+ const parsed = rows.map(
886
+ (row) => OrganizationDocumentAuthorityReclassification.parse(row.result)
887
+ );
888
+ const hasMore = parsed.length > limit;
889
+ const receipts = parsed.slice(0, limit);
890
+ const tail = hasMore ? receipts.at(-1) : null;
891
+ return ListOrganizationDocumentAuthorityReclassificationsResponse.parse({
892
+ receipts,
893
+ hasMore,
894
+ nextCursor: tail ? encodeDocumentMigrationAuditCursor(
895
+ ["organization-reclassifications", input.accountId],
896
+ { timestamp: tail.createdAt, id: tail.operationId }
897
+ ) : null
898
+ });
899
+ }
900
+ );
901
+ }
902
+ function validateDocumentMigrationAuditLimit(value) {
903
+ const limit = value ?? 50;
904
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
905
+ throw new Error("document migration audit limit must be between 1 and 100");
906
+ }
907
+ return limit;
908
+ }
909
+ function documentMigrationAuditCursorScope(parts) {
910
+ return createHash("sha256").update(JSON.stringify(["document-migration-audit", 1, ...parts]), "utf8").digest("hex").slice(0, 32);
911
+ }
912
+ function encodeDocumentMigrationAuditCursor(parts, cursor) {
913
+ return Buffer.from(
914
+ JSON.stringify({
915
+ v: 1,
916
+ s: documentMigrationAuditCursorScope(parts),
917
+ t: cursor.timestamp,
918
+ i: cursor.id
919
+ }),
920
+ "utf8"
921
+ ).toString("base64url");
922
+ }
923
+ function decodeDocumentMigrationAuditCursor(value, parts, requireTimestamp) {
924
+ try {
925
+ if (!value || value.length > 1024) throw new Error("cursor length");
926
+ const bytes = Buffer.from(value, "base64url");
927
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
928
+ const parsed = JSON.parse(bytes.toString("utf8"));
929
+ if (Object.keys(parsed).sort().join(",") !== "i,s,t,v" || parsed.v !== 1 || parsed.s !== documentMigrationAuditCursorScope(parts) || typeof parsed.i !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
930
+ parsed.i
931
+ ) || (requireTimestamp ? typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime()) : parsed.t !== null)) {
932
+ throw new Error("cursor payload");
933
+ }
934
+ return { timestamp: typeof parsed.t === "string" ? parsed.t : null, id: parsed.i };
935
+ } catch (error) {
936
+ throw new Error("invalid document migration audit cursor", { cause: error });
937
+ }
938
+ }
939
+ async function reclassifyDocumentAuthority(db, input) {
940
+ return await withDocumentAccountRls(
941
+ db,
942
+ input.accountId,
943
+ input.workspaceId,
944
+ { viewerSubjectId: input.actorSubjectId },
945
+ async (scopedDb) => {
946
+ const command = {
947
+ accountId: input.accountId,
948
+ workspaceId: input.workspaceId,
949
+ documentId: input.documentId,
950
+ operationId: input.operationId,
951
+ actorSubjectId: input.actorSubjectId,
952
+ expectedAuthority: input.expectedAuthority,
953
+ targetAuthorityKind: input.targetAuthorityKind,
954
+ accountAdminAuthorization: input.accountAdminAuthorization
955
+ };
956
+ const rows = await scopedDb.execute(sql`
957
+ SELECT reclassify_document_authority(${JSON.stringify(command)}::jsonb) AS result
958
+ `);
959
+ const row = rows[0];
960
+ if (!row) throw new Error("Document authority reclassification returned no result");
961
+ return DocumentAuthorityReclassification.parse(row.result);
962
+ }
963
+ );
964
+ }
965
+ async function listDocumentAuthorityReclassifications(db, input) {
966
+ const limit = input.limit ?? 50;
967
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
968
+ throw new Error("document authority receipt limit must be between 1 and 100");
969
+ }
970
+ const cursor = input.cursor ? decodeDocumentAuthorityReclassificationCursor(input.cursor, input) : null;
971
+ return await withDocumentAccountRls(
972
+ db,
973
+ input.accountId,
974
+ input.workspaceId,
975
+ { viewerSubjectId: input.actorSubjectId },
976
+ async (scopedDb) => {
977
+ const rows = await scopedDb.execute(sql`
978
+ SELECT list_document_authority_reclassifications(
979
+ ${input.accountId}::uuid,
980
+ ${input.workspaceId}::uuid,
981
+ ${input.actorSubjectId},
982
+ ${input.documentId}::uuid,
983
+ ${limit + 1}::integer,
984
+ ${cursor?.createdAt ?? null}::timestamptz,
985
+ ${cursor?.operationId ?? null}::uuid
986
+ ) AS result
987
+ `);
988
+ const parsed = rows.map((row) => DocumentAuthorityReclassification.parse(row.result));
989
+ const hasMore = parsed.length > limit;
990
+ const receipts = parsed.slice(0, limit);
991
+ const tail = hasMore ? receipts.at(-1) : null;
992
+ return ListDocumentAuthorityReclassificationsResponse.parse({
993
+ receipts,
994
+ hasMore,
995
+ nextCursor: tail ? encodeDocumentAuthorityReclassificationCursor(input, {
996
+ createdAt: tail.createdAt,
997
+ operationId: tail.operationId
998
+ }) : null
999
+ });
1000
+ }
1001
+ );
1002
+ }
1003
+ function documentAuthorityReclassificationCursorScope(input) {
1004
+ return createHash("sha256").update(
1005
+ JSON.stringify([
1006
+ "document_authority_reclassification_cursor",
1007
+ 1,
1008
+ input.accountId,
1009
+ input.workspaceId,
1010
+ input.documentId,
1011
+ input.actorSubjectId
1012
+ ]),
1013
+ "utf8"
1014
+ ).digest("hex").slice(0, 32);
1015
+ }
1016
+ function encodeDocumentAuthorityReclassificationCursor(scope, cursor) {
1017
+ return Buffer.from(
1018
+ JSON.stringify({
1019
+ v: 1,
1020
+ s: documentAuthorityReclassificationCursorScope(scope),
1021
+ t: cursor.createdAt,
1022
+ i: cursor.operationId
1023
+ }),
1024
+ "utf8"
1025
+ ).toString("base64url");
1026
+ }
1027
+ function decodeDocumentAuthorityReclassificationCursor(value, scope) {
1028
+ try {
1029
+ if (!value || value.length > 1024) throw new Error("cursor length");
1030
+ const bytes = Buffer.from(value, "base64url");
1031
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1032
+ const parsed = JSON.parse(bytes.toString("utf8"));
1033
+ if (Object.keys(parsed).sort().join(",") !== "i,s,t,v" || parsed.v !== 1 || parsed.s !== documentAuthorityReclassificationCursorScope(scope) || typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime()) || typeof parsed.i !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed.i)) {
1034
+ throw new Error("cursor payload");
1035
+ }
1036
+ return { createdAt: parsed.t, operationId: parsed.i };
1037
+ } catch (error) {
1038
+ throw new Error("invalid document authority receipt cursor", { cause: error });
1039
+ }
1040
+ }
700
1041
  async function addDocumentToBase(db, input) {
701
1042
  return await withRlsContext(
702
1043
  db,
@@ -776,33 +1117,47 @@ async function addDocumentToBase(db, input) {
776
1117
  ).returning();
777
1118
  return mapDocument(updated ?? existing);
778
1119
  }
779
- const [row] = await scopedDb.insert(schema.documents).values({
780
- accountId: input.accountId,
781
- workspaceId: input.workspaceId,
782
- baseId: input.baseId,
783
- fileId: input.fileId,
784
- status: "queued",
785
- title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
786
- parser: DEFAULT_DOCUMENT_PARSER,
787
- sourceKind: input.sourceKind ?? "manual_upload",
788
- sourceUri: cleanString(input.sourceUri) ?? null,
789
- sourceExternalId: cleanString(input.sourceExternalId) ?? null,
790
- sourceTitle: cleanString(input.sourceTitle) ?? null,
791
- sourceAuthor: cleanString(input.sourceAuthor) ?? null,
792
- sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
793
- sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
794
- sourceVersion: cleanString(input.sourceVersion) ?? null,
795
- knowledgeSourceIdentity,
796
- aclTags: cleanStringArray(input.aclTags),
797
- authorityKind: authority.kind,
798
- authorityWorkspaceId: authority.workspaceId,
799
- authoritySubjectId: authority.subjectId,
800
- visibility: authority.kind === "personal" ? "private" : "workspace",
801
- agentAccess: input.agentAccess ?? true,
802
- createdBy: fileAuthoritySubjectId,
803
- curationStatus: input.curationStatus ?? "none",
804
- updatedAt: now
805
- }).returning();
1120
+ const documentId = randomUUID();
1121
+ const row = await scopedDb.transaction(async (tx) => {
1122
+ const userAuthority = authority.kind === "personal" ? await createPersonalDocumentAuthority(tx, {
1123
+ accountId: input.accountId,
1124
+ workspaceId: input.workspaceId,
1125
+ subjectId: authority.subjectId,
1126
+ documentId
1127
+ }) : null;
1128
+ const [inserted] = await tx.insert(schema.documents).values({
1129
+ id: documentId,
1130
+ accountId: input.accountId,
1131
+ workspaceId: input.workspaceId,
1132
+ baseId: input.baseId,
1133
+ fileId: input.fileId,
1134
+ status: "queued",
1135
+ title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
1136
+ parser: DEFAULT_DOCUMENT_PARSER,
1137
+ sourceKind: input.sourceKind ?? "manual_upload",
1138
+ sourceUri: cleanString(input.sourceUri) ?? null,
1139
+ sourceExternalId: cleanString(input.sourceExternalId) ?? null,
1140
+ sourceTitle: cleanString(input.sourceTitle) ?? null,
1141
+ sourceAuthor: cleanString(input.sourceAuthor) ?? null,
1142
+ sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
1143
+ sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
1144
+ sourceVersion: cleanString(input.sourceVersion) ?? null,
1145
+ knowledgeSourceIdentity,
1146
+ aclTags: cleanStringArray(input.aclTags),
1147
+ authorityKind: authority.kind,
1148
+ authorityWorkspaceId: userAuthority ? null : authority.workspaceId,
1149
+ authoritySubjectId: authority.subjectId,
1150
+ authorityId: userAuthority?.authorityId ?? null,
1151
+ ownerOrganizationMembershipId: userAuthority?.ownerOrganizationMembershipId ?? null,
1152
+ originWorkspaceId: input.workspaceId,
1153
+ visibility: authority.kind === "personal" ? "private" : "workspace",
1154
+ agentAccess: input.agentAccess ?? true,
1155
+ createdBy: fileAuthoritySubjectId,
1156
+ curationStatus: input.curationStatus ?? "none",
1157
+ updatedAt: now
1158
+ }).returning();
1159
+ return inserted;
1160
+ });
806
1161
  if (!row) throw new Error("Failed to create document");
807
1162
  return mapDocument(row);
808
1163
  }
@@ -817,7 +1172,7 @@ async function moveDocumentToBase(db, input) {
817
1172
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
818
1173
  const [row] = await scopedDb.select().from(schema.documents).where(
819
1174
  and(
820
- eq(schema.documents.workspaceId, input.workspaceId),
1175
+ eq(schema.documents.accountId, input.accountId),
821
1176
  eq(schema.documents.id, input.documentId),
822
1177
  ...documentAccessConditions(input.workspaceId, input.access)
823
1178
  )
@@ -830,11 +1185,11 @@ async function moveDocumentToBase(db, input) {
830
1185
  throw new Error("document has no suggested base; pass targetBaseId");
831
1186
  }
832
1187
  if (targetBaseId === row.baseId) return mapDocument(row);
833
- const base = await getDocumentBase(scopedDb, input.workspaceId, targetBaseId);
1188
+ const base = await getDocumentBase(scopedDb, row.workspaceId, targetBaseId);
834
1189
  if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
835
1190
  const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
836
1191
  and(
837
- eq(schema.documents.workspaceId, input.workspaceId),
1192
+ eq(schema.documents.workspaceId, row.workspaceId),
838
1193
  eq(schema.documents.baseId, targetBaseId),
839
1194
  ...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
840
1195
  )
@@ -850,7 +1205,7 @@ async function moveDocumentToBase(db, input) {
850
1205
  updatedAt: now
851
1206
  }).where(
852
1207
  and(
853
- eq(schema.documents.workspaceId, input.workspaceId),
1208
+ eq(schema.documents.workspaceId, row.workspaceId),
854
1209
  eq(schema.documents.id, input.documentId),
855
1210
  ...documentAccessConditions(input.workspaceId, input.access)
856
1211
  )
@@ -858,7 +1213,7 @@ async function moveDocumentToBase(db, input) {
858
1213
  if (updated) {
859
1214
  await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
860
1215
  and(
861
- eq(schema.documentChunks.workspaceId, input.workspaceId),
1216
+ eq(schema.documentChunks.workspaceId, row.workspaceId),
862
1217
  eq(schema.documentChunks.documentId, input.documentId)
863
1218
  )
864
1219
  );
@@ -879,7 +1234,7 @@ async function deleteDocumentFromBase(db, input) {
879
1234
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
880
1235
  const [document] = await scopedDb.select().from(schema.documents).where(
881
1236
  and(
882
- eq(schema.documents.workspaceId, input.workspaceId),
1237
+ eq(schema.documents.accountId, input.accountId),
883
1238
  eq(schema.documents.id, input.documentId),
884
1239
  ...documentAccessConditions(input.workspaceId, input.access)
885
1240
  )
@@ -896,7 +1251,7 @@ async function deleteDocumentFromBase(db, input) {
896
1251
  }
897
1252
  await scopedDb.delete(schema.documents).where(
898
1253
  and(
899
- eq(schema.documents.workspaceId, input.workspaceId),
1254
+ eq(schema.documents.accountId, input.accountId),
900
1255
  eq(schema.documents.id, input.documentId),
901
1256
  ...documentAccessConditions(input.workspaceId, input.access)
902
1257
  )
@@ -916,6 +1271,12 @@ async function listDocuments(db, workspaceId, baseId, access) {
916
1271
  return rows.map(mapDocument);
917
1272
  });
918
1273
  }
1274
+ async function listAccessibleDocuments(db, workspaceId, access) {
1275
+ return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1276
+ const rows = await scopedDb.select().from(schema.documents).where(and(...documentAccessConditions(workspaceId, access))).orderBy(desc(schema.documents.updatedAt), asc(schema.documents.createdAt));
1277
+ return rows.map(mapDocument);
1278
+ });
1279
+ }
919
1280
  async function listEffectiveIndexedDocuments(db, input) {
920
1281
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
921
1282
  const limit = input.limit ?? 50;
@@ -927,10 +1288,13 @@ async function listEffectiveIndexedDocuments(db, input) {
927
1288
  workspaceId: input.workspaceId,
928
1289
  initiatingSubjectId
929
1290
  }) : 0n;
930
- const access = {
931
- agentOnly: true,
932
- viewerSubjectId: initiatingSubjectId
933
- };
1291
+ const access = await resolveEffectiveDocumentAccess(db, {
1292
+ accountId: input.accountId,
1293
+ workspaceId: input.workspaceId,
1294
+ initiatingSubjectId,
1295
+ surface: "agent",
1296
+ agentAuthority: input.agentAuthority
1297
+ });
934
1298
  const rows = await withDocumentAccountRls(
935
1299
  db,
936
1300
  input.accountId,
@@ -1014,15 +1378,21 @@ function canonicalEffectiveDocumentSubject(value) {
1014
1378
  async function getDocument(db, workspaceId, documentId, access) {
1015
1379
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1016
1380
  const [row] = await scopedDb.select().from(schema.documents).where(
1017
- and(
1018
- eq(schema.documents.workspaceId, workspaceId),
1019
- eq(schema.documents.id, documentId),
1020
- ...documentAccessConditions(workspaceId, access)
1021
- )
1381
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1022
1382
  ).limit(1);
1023
1383
  return row ? mapDocument(row) : null;
1024
1384
  });
1025
1385
  }
1386
+ async function getDocumentOriginalFile(db, input) {
1387
+ const subjectId = cleanString(input.access.viewerSubjectId ?? null);
1388
+ if (!subjectId || input.access.agentOnly) return null;
1389
+ return await resolveDocumentOriginalFileForSubject(db, {
1390
+ accountId: input.accountId,
1391
+ workspaceId: input.workspaceId,
1392
+ subjectId,
1393
+ documentId: input.documentId
1394
+ });
1395
+ }
1026
1396
  async function getDocumentForIndexing(db, workspaceId, documentId) {
1027
1397
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1028
1398
  const [row] = await scopedDb.select().from(schema.documents).where(
@@ -1033,12 +1403,8 @@ async function getDocumentForIndexing(db, workspaceId, documentId) {
1033
1403
  }
1034
1404
  async function queueDocumentForReindex(db, workspaceId, documentId, access, organizationAuthorityGranted) {
1035
1405
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1036
- const [document] = await scopedDb.select({ authorityKind: schema.documents.authorityKind }).from(schema.documents).where(
1037
- and(
1038
- eq(schema.documents.workspaceId, workspaceId),
1039
- eq(schema.documents.id, documentId),
1040
- ...documentAccessConditions(workspaceId, access)
1041
- )
1406
+ const [document] = await scopedDb.select().from(schema.documents).where(
1407
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1042
1408
  ).limit(1);
1043
1409
  if (!document) throw new Error(`Document not found: ${documentId}`);
1044
1410
  assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
@@ -1047,11 +1413,7 @@ async function queueDocumentForReindex(db, workspaceId, documentId, access, orga
1047
1413
  error: null,
1048
1414
  updatedAt: /* @__PURE__ */ new Date()
1049
1415
  }).where(
1050
- and(
1051
- eq(schema.documents.workspaceId, workspaceId),
1052
- eq(schema.documents.id, documentId),
1053
- ...documentAccessConditions(workspaceId, access)
1054
- )
1416
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1055
1417
  ).returning();
1056
1418
  if (!row) throw new Error(`Document not found: ${documentId}`);
1057
1419
  return mapDocument(row);
@@ -1090,7 +1452,11 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1090
1452
  );
1091
1453
  });
1092
1454
  try {
1093
- const bytes = await objectStorage.getFileBytes(file);
1455
+ const object = await retryWhileMissing(
1456
+ async () => objectStorage.getObjectBytes(file.objectKey)
1457
+ );
1458
+ if (!object) throw new Error("document source object is missing");
1459
+ const bytes = object.bytes;
1094
1460
  const parsed = await services.parser.parse(bytes, file);
1095
1461
  if (document.curationStatus === "pending") {
1096
1462
  document = await curateDroppedDocument(db, services, document, parsed, file);
@@ -1284,6 +1650,9 @@ async function curateDroppedDocument(db, services, document, parsed, file) {
1284
1650
  return updated ?? document;
1285
1651
  }
1286
1652
  async function searchDocuments(db, input, services = createDocumentServices()) {
1653
+ return (await searchDocumentCandidates(db, input, services)).results;
1654
+ }
1655
+ async function searchDocumentCandidates(db, input, services, relevanceFloor) {
1287
1656
  await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
1288
1657
  const mode = input.mode ?? "hybrid";
1289
1658
  const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
@@ -1308,13 +1677,34 @@ async function searchDocuments(db, input, services = createDocumentServices()) {
1308
1677
  if (mode === "keyword" || mode === "hybrid") {
1309
1678
  rows.push(...await keywordSearchDocuments(db, input, candidateLimit));
1310
1679
  }
1311
- return mergeDocumentSearchRows(rows, mode).slice(0, limit);
1680
+ const merged = mergeDocumentSearchRows(rows, mode);
1681
+ return selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor);
1682
+ }
1683
+ function selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor) {
1684
+ const boundedLimit = Math.min(Math.max(Math.trunc(limit), 1), 50);
1685
+ if (!relevanceFloor) {
1686
+ return { results: merged.slice(0, boundedLimit), belowRelevanceFloor: 0 };
1687
+ }
1688
+ let belowRelevanceFloor = 0;
1689
+ const relevant = merged.filter((result) => {
1690
+ const included = result.vectorScore !== null && result.vectorScore >= relevanceFloor.vectorScore || result.keywordScore !== null && result.keywordScore >= relevanceFloor.keywordScore;
1691
+ if (!included) belowRelevanceFloor += 1;
1692
+ return included;
1693
+ });
1694
+ return { results: relevant.slice(0, boundedLimit), belowRelevanceFloor };
1312
1695
  }
1313
1696
  async function searchEffectiveDocuments(db, input, services = createDocumentServices()) {
1314
1697
  const initiatingSubjectId = cleanString(input.initiatingSubjectId);
1315
1698
  if (!initiatingSubjectId) {
1316
1699
  throw new Error("effective document retrieval requires an initiating subject");
1317
1700
  }
1701
+ const access = await resolveEffectiveDocumentAccess(db, {
1702
+ accountId: input.accountId,
1703
+ workspaceId: input.workspaceId,
1704
+ initiatingSubjectId,
1705
+ surface: input.surface,
1706
+ agentAuthority: input.agentAuthority
1707
+ });
1318
1708
  return await searchDocuments(
1319
1709
  db,
1320
1710
  {
@@ -1328,23 +1718,50 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
1328
1718
  aclTags: input.aclTags,
1329
1719
  // Construct the lower-level access filter here instead of spreading the
1330
1720
  // caller input, so an untyped/legacy access override is always ignored.
1331
- access: {
1332
- viewerSubjectId: initiatingSubjectId,
1333
- ...input.surface === "agent" ? { agentOnly: true } : {}
1334
- }
1721
+ access
1335
1722
  },
1336
1723
  services
1337
1724
  );
1338
1725
  }
1339
1726
  async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
1340
1727
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1341
- const ranked = await searchEffectiveDocuments(
1728
+ const requestedLimit = Math.min(Math.max(input.limit ?? 5, 1), KNOWLEDGE_SEARCH_MAX_RESULTS);
1729
+ const access = await resolveEffectiveDocumentAccess(db, {
1730
+ accountId: input.accountId,
1731
+ workspaceId: input.workspaceId,
1732
+ initiatingSubjectId,
1733
+ surface: input.surface,
1734
+ agentAuthority: input.agentAuthority
1735
+ });
1736
+ const candidateLimit = Math.min(requestedLimit * 4, KNOWLEDGE_SEARCH_MAX_RESULTS);
1737
+ const rankedSelection = await searchDocumentCandidates(
1342
1738
  db,
1343
- { ...input, initiatingSubjectId, surface: "agent" },
1344
- services
1739
+ {
1740
+ accountId: input.accountId,
1741
+ workspaceId: input.workspaceId,
1742
+ query: input.query,
1743
+ limit: candidateLimit,
1744
+ ...input.baseIds ? { baseIds: input.baseIds } : {},
1745
+ ...input.mode ? { mode: input.mode } : {},
1746
+ ...input.sourceKinds ? { sourceKinds: input.sourceKinds } : {},
1747
+ ...input.aclTags ? { aclTags: input.aclTags } : {},
1748
+ access
1749
+ },
1750
+ services,
1751
+ {
1752
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1753
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1754
+ }
1345
1755
  );
1346
- if (ranked.length === 0) return { results: [] };
1347
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1756
+ const ranked = rankedSelection.results;
1757
+ if (ranked.length === 0) {
1758
+ return selectKnowledgeSearchResults({
1759
+ candidates: [],
1760
+ rankedCandidateCount: 0,
1761
+ requestedLimit,
1762
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor
1763
+ });
1764
+ }
1348
1765
  const current = await withDocumentAccountRls(
1349
1766
  db,
1350
1767
  input.accountId,
@@ -1353,7 +1770,9 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
1353
1770
  async (scopedDb) => await scopedDb.select({
1354
1771
  chunk: schema.documentChunks,
1355
1772
  document: schema.documents,
1356
- citation: googleDriveCitationProjection(input.workspaceId, access)
1773
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1774
+ previousChunkId: knowledgePreviousChunkIdProjection(),
1775
+ nextChunkId: knowledgeNextChunkIdProjection()
1357
1776
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1358
1777
  and(
1359
1778
  eq(schema.documents.accountId, input.accountId),
@@ -1368,28 +1787,310 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
1368
1787
  )
1369
1788
  );
1370
1789
  const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
1371
- return {
1372
- results: ranked.flatMap((rankedResult) => {
1790
+ return selectKnowledgeSearchResults({
1791
+ rankedCandidateCount: ranked.length,
1792
+ requestedLimit,
1793
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor,
1794
+ candidates: ranked.flatMap((rankedResult) => {
1373
1795
  const row = currentByChunkId.get(rankedResult.chunkId);
1374
1796
  if (!row) return [];
1375
1797
  return [
1376
1798
  {
1377
- record: knowledgeChunkRecord(row.document, row.chunk, row.citation),
1378
- retrieval: {
1379
- score: rankedResult.score,
1380
- matchType: rankedResult.matchType,
1381
- vectorScore: rankedResult.vectorScore,
1382
- keywordScore: rankedResult.keywordScore
1383
- }
1799
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
1800
+ previousChunkId: row.previousChunkId,
1801
+ nextChunkId: row.nextChunkId
1802
+ }),
1803
+ semanticScore: rankedResult.score,
1804
+ matchType: rankedResult.matchType,
1805
+ vectorScore: rankedResult.vectorScore,
1806
+ keywordScore: rankedResult.keywordScore
1384
1807
  }
1385
1808
  ];
1386
1809
  })
1810
+ });
1811
+ }
1812
+ function selectKnowledgeSearchResults(input) {
1813
+ const requestedLimit = Math.min(
1814
+ Math.max(Math.trunc(input.requestedLimit), 1),
1815
+ KNOWLEDGE_SEARCH_MAX_RESULTS
1816
+ );
1817
+ const nowMs = (input.now ?? /* @__PURE__ */ new Date()).getTime();
1818
+ let belowRelevanceFloor = Math.min(
1819
+ Math.max(0, Math.trunc(input.alreadyBelowRelevanceFloor ?? 0)),
1820
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS
1821
+ );
1822
+ const relevant = [];
1823
+ for (const candidate of input.candidates) {
1824
+ const relevanceSignals = [];
1825
+ if (candidate.vectorScore !== null && candidate.vectorScore >= KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE) {
1826
+ relevanceSignals.push("vector");
1827
+ }
1828
+ if (candidate.keywordScore !== null && candidate.keywordScore >= KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE) {
1829
+ relevanceSignals.push("keyword");
1830
+ }
1831
+ if (relevanceSignals.length === 0) {
1832
+ belowRelevanceFloor = Math.min(belowRelevanceFloor + 1, KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS);
1833
+ continue;
1834
+ }
1835
+ const freshness = knowledgeFreshness(candidate.record.quality.freshnessAt, nowMs);
1836
+ const qualityAdjustment = freshness === "current" ? 0.02 : freshness === "aging" ? 0.01 : 0;
1837
+ relevant.push({
1838
+ record: candidate.record,
1839
+ retrieval: {
1840
+ score: roundScore(Math.min(1, candidate.semanticScore + qualityAdjustment)),
1841
+ semanticScore: roundScore(candidate.semanticScore),
1842
+ matchType: candidate.matchType,
1843
+ vectorScore: candidate.vectorScore === null ? null : roundScore(candidate.vectorScore),
1844
+ keywordScore: candidate.keywordScore === null ? null : roundScore(candidate.keywordScore),
1845
+ relevanceSignals,
1846
+ freshness,
1847
+ qualityAdjustment,
1848
+ duplicateCount: 0
1849
+ }
1850
+ });
1851
+ }
1852
+ relevant.sort(compareKnowledgeSearchResults);
1853
+ const deduped = [];
1854
+ const byContent = /* @__PURE__ */ new Map();
1855
+ let asDuplicate = 0;
1856
+ for (const result of relevant) {
1857
+ const key = knowledgeTextualContentKey(result.record);
1858
+ const retainedIndex = byContent.get(key);
1859
+ if (retainedIndex === void 0) {
1860
+ byContent.set(key, deduped.length);
1861
+ deduped.push(result);
1862
+ continue;
1863
+ }
1864
+ asDuplicate += 1;
1865
+ const retained = deduped[retainedIndex];
1866
+ retained.retrieval.duplicateCount += 1;
1867
+ }
1868
+ const forLimit = Math.max(0, deduped.length - requestedLimit);
1869
+ const bounded = deduped.slice(0, requestedLimit);
1870
+ let forResponseBudget = 0;
1871
+ let response = knowledgeSearchResponse({
1872
+ results: bounded,
1873
+ rankedCandidateCount: input.rankedCandidateCount,
1874
+ recheckedCandidateCount: input.candidates.length,
1875
+ belowRelevanceFloor,
1876
+ asDuplicate,
1877
+ forLimit,
1878
+ forResponseBudget
1879
+ });
1880
+ while (knowledgeResponseBytes(response) > KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES) {
1881
+ if (bounded.length === 0) {
1882
+ throw new Error("knowledge search selection facts exceed the response budget");
1883
+ }
1884
+ bounded.pop();
1885
+ forResponseBudget += 1;
1886
+ response = knowledgeSearchResponse({
1887
+ results: bounded,
1888
+ rankedCandidateCount: input.rankedCandidateCount,
1889
+ recheckedCandidateCount: input.candidates.length,
1890
+ belowRelevanceFloor,
1891
+ asDuplicate,
1892
+ forLimit,
1893
+ forResponseBudget
1894
+ });
1895
+ }
1896
+ return response;
1897
+ }
1898
+ function compareKnowledgeSearchResults(left, right) {
1899
+ return right.retrieval.score - left.retrieval.score || right.retrieval.semanticScore - left.retrieval.semanticScore || (right.retrieval.vectorScore ?? 0) - (left.retrieval.vectorScore ?? 0) || (right.retrieval.keywordScore ?? 0) - (left.retrieval.keywordScore ?? 0) || (left.record.id === right.record.id ? 0 : left.record.id < right.record.id ? -1 : 1);
1900
+ }
1901
+ function knowledgeFreshness(value, nowMs) {
1902
+ const freshnessMs = Date.parse(value);
1903
+ if (!Number.isFinite(freshnessMs)) return "stale";
1904
+ const ageDays = Math.max(0, (nowMs - freshnessMs) / 864e5);
1905
+ if (ageDays <= 90) return "current";
1906
+ return ageDays <= 365 ? "aging" : "stale";
1907
+ }
1908
+ function knowledgeTextualContentKey(record) {
1909
+ return createHash("sha256").update("opengeni:knowledge-search-content:v1\0").update(record.title).update("\0").update(
1910
+ JSON.stringify({
1911
+ body: record.content.body,
1912
+ summary: record.content.summary,
1913
+ topics: record.content.topics
1914
+ })
1915
+ ).digest("hex");
1916
+ }
1917
+ function knowledgeSearchResponse(input) {
1918
+ const selection = {
1919
+ relevanceFloor: {
1920
+ policy: "any_signal",
1921
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1922
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1923
+ },
1924
+ dedupe: { policy: "exact_textual_content" },
1925
+ candidates: {
1926
+ ranked: input.rankedCandidateCount,
1927
+ rechecked: input.recheckedCandidateCount,
1928
+ omittedOnRecheck: Math.max(0, input.rankedCandidateCount - input.recheckedCandidateCount)
1929
+ },
1930
+ omitted: {
1931
+ belowRelevanceFloor: input.belowRelevanceFloor,
1932
+ asDuplicate: input.asDuplicate,
1933
+ forLimit: input.forLimit,
1934
+ forResponseBudget: input.forResponseBudget
1935
+ },
1936
+ budget: {
1937
+ maxResults: KNOWLEDGE_SEARCH_MAX_RESULTS,
1938
+ maxResponseBytes: KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
1939
+ responseBytes: 0,
1940
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
1941
+ estimatedTokens: 0,
1942
+ maxEstimatedTokens: Math.ceil(
1943
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1944
+ )
1945
+ }
1387
1946
  };
1947
+ const response = {
1948
+ results: [...input.results],
1949
+ selection
1950
+ };
1951
+ for (let index = 0; index < 8; index += 1) {
1952
+ const responseBytes = knowledgeResponseBytes(response);
1953
+ const estimatedTokens = Math.ceil(
1954
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1955
+ );
1956
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
1957
+ break;
1958
+ }
1959
+ response.selection.budget.responseBytes = responseBytes;
1960
+ response.selection.budget.estimatedTokens = estimatedTokens;
1961
+ }
1962
+ return response;
1963
+ }
1964
+ function knowledgeResponseBytes(response) {
1965
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1966
+ }
1967
+ function selectKnowledgeBrowseRecords(input) {
1968
+ const entries = input.entries.slice(0, KNOWLEDGE_BROWSE_MAX_LIMIT);
1969
+ const selected = entries.map((entry) => entry.record);
1970
+ let omittedForResponseBudget = 0;
1971
+ let compactedRecordCount = 0;
1972
+ let response = knowledgeBrowseResponse({
1973
+ records: selected,
1974
+ nextCursor: input.hasMoreAfterEntries ? entries.at(-1)?.cursorAfter ?? null : null,
1975
+ hasMore: input.hasMoreAfterEntries,
1976
+ omittedForResponseBudget,
1977
+ compactedRecordCount
1978
+ });
1979
+ while (selected.length > 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1980
+ selected.pop();
1981
+ omittedForResponseBudget += 1;
1982
+ response = knowledgeBrowseResponse({
1983
+ records: selected,
1984
+ nextCursor: entries[selected.length - 1]?.cursorAfter ?? null,
1985
+ hasMore: true,
1986
+ omittedForResponseBudget,
1987
+ compactedRecordCount
1988
+ });
1989
+ }
1990
+ if (selected.length === 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1991
+ selected[0] = compactKnowledgeBrowseRecord(selected[0]);
1992
+ compactedRecordCount = 1;
1993
+ response = knowledgeBrowseResponse({
1994
+ records: selected,
1995
+ nextCursor: omittedForResponseBudget > 0 || input.hasMoreAfterEntries ? entries[0]?.cursorAfter ?? null : null,
1996
+ hasMore: omittedForResponseBudget > 0 || input.hasMoreAfterEntries,
1997
+ omittedForResponseBudget,
1998
+ compactedRecordCount
1999
+ });
2000
+ }
2001
+ if (knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
2002
+ throw new Error("knowledge browse discovery projection exceeds the response budget");
2003
+ }
2004
+ return response;
2005
+ }
2006
+ function compactKnowledgeBrowseRecord(record) {
2007
+ const fields = /* @__PURE__ */ new Set([
2008
+ ...record.projection.fields,
2009
+ "content.body",
2010
+ "content.summary",
2011
+ "content.topics",
2012
+ "content.metadata",
2013
+ "provenance.source.uri",
2014
+ "provenance.source.externalId",
2015
+ "provenance.source.title",
2016
+ "provenance.source.author",
2017
+ "provenance.source.version",
2018
+ "provenance.citation"
2019
+ ]);
2020
+ return {
2021
+ ...record,
2022
+ content: {
2023
+ format: "markdown",
2024
+ body: null,
2025
+ summary: null,
2026
+ topics: [],
2027
+ metadata: {}
2028
+ },
2029
+ provenance: {
2030
+ ...record.provenance,
2031
+ source: {
2032
+ ...record.provenance.source,
2033
+ uri: null,
2034
+ externalId: null,
2035
+ title: null,
2036
+ author: null,
2037
+ version: null
2038
+ },
2039
+ citation: null
2040
+ },
2041
+ links: record.links.filter((link) => link.target.kind === "knowledge"),
2042
+ projection: {
2043
+ truncated: true,
2044
+ fields: [...fields].sort()
2045
+ }
2046
+ };
2047
+ }
2048
+ function knowledgeBrowseResponse(input) {
2049
+ const response = {
2050
+ records: [...input.records],
2051
+ nextCursor: input.nextCursor,
2052
+ hasMore: input.hasMore,
2053
+ selection: {
2054
+ omitted: { forResponseBudget: input.omittedForResponseBudget },
2055
+ compactedRecordCount: input.compactedRecordCount,
2056
+ budget: {
2057
+ maxResults: KNOWLEDGE_BROWSE_MAX_LIMIT,
2058
+ maxResponseBytes: KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
2059
+ responseBytes: 0,
2060
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
2061
+ estimatedTokens: 0,
2062
+ maxEstimatedTokens: Math.ceil(
2063
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
2064
+ )
2065
+ }
2066
+ }
2067
+ };
2068
+ for (let index = 0; index < 8; index += 1) {
2069
+ const responseBytes = knowledgeBrowseResponseBytes(response);
2070
+ const estimatedTokens = Math.ceil(
2071
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
2072
+ );
2073
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
2074
+ break;
2075
+ }
2076
+ response.selection.budget.responseBytes = responseBytes;
2077
+ response.selection.budget.estimatedTokens = estimatedTokens;
2078
+ }
2079
+ return response;
2080
+ }
2081
+ function knowledgeBrowseResponseBytes(response) {
2082
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1388
2083
  }
1389
2084
  async function getEffectiveKnowledgeRecord(db, input) {
1390
2085
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1391
2086
  const target = parseKnowledgeRecordId(input.id);
1392
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
2087
+ const access = await resolveEffectiveDocumentAccess(db, {
2088
+ accountId: input.accountId,
2089
+ workspaceId: input.workspaceId,
2090
+ initiatingSubjectId,
2091
+ surface: input.surface ?? "agent",
2092
+ agentAuthority: input.agentAuthority
2093
+ });
1393
2094
  return await withDocumentAccountRls(
1394
2095
  db,
1395
2096
  input.accountId,
@@ -1399,8 +2100,16 @@ async function getEffectiveKnowledgeRecord(db, input) {
1399
2100
  if (target.kind === "document") {
1400
2101
  const [row2] = await scopedDb.select({
1401
2102
  document: schema.documents,
1402
- citation: googleDriveCitationProjection(input.workspaceId, access)
1403
- }).from(schema.documents).where(
2103
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2104
+ firstChunkId: schema.documentChunks.id
2105
+ }).from(schema.documents).leftJoin(
2106
+ schema.documentChunks,
2107
+ and(
2108
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
2109
+ eq(schema.documentChunks.documentId, schema.documents.id),
2110
+ eq(schema.documentChunks.chunkIndex, 0)
2111
+ )
2112
+ ).where(
1404
2113
  and(
1405
2114
  eq(schema.documents.accountId, input.accountId),
1406
2115
  eq(schema.documents.id, target.id),
@@ -1408,12 +2117,14 @@ async function getEffectiveKnowledgeRecord(db, input) {
1408
2117
  ...documentAccessConditions(input.workspaceId, access)
1409
2118
  )
1410
2119
  ).limit(1);
1411
- return row2 ? knowledgeDocumentRecord(row2.document, row2.citation) : null;
2120
+ return row2 ? knowledgeDocumentRecord(row2.document, row2.citation, row2.firstChunkId) : null;
1412
2121
  }
1413
2122
  const [row] = await scopedDb.select({
1414
2123
  chunk: schema.documentChunks,
1415
2124
  document: schema.documents,
1416
- citation: googleDriveCitationProjection(input.workspaceId, access)
2125
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2126
+ previousChunkId: knowledgePreviousChunkIdProjection(),
2127
+ nextChunkId: knowledgeNextChunkIdProjection()
1417
2128
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1418
2129
  and(
1419
2130
  eq(schema.documents.accountId, input.accountId),
@@ -1423,7 +2134,10 @@ async function getEffectiveKnowledgeRecord(db, input) {
1423
2134
  ...documentAccessConditions(input.workspaceId, access)
1424
2135
  )
1425
2136
  ).limit(1);
1426
- return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation) : null;
2137
+ return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation, {
2138
+ previousChunkId: row.previousChunkId,
2139
+ nextChunkId: row.nextChunkId
2140
+ }) : null;
1427
2141
  }
1428
2142
  );
1429
2143
  }
@@ -1453,11 +2167,14 @@ async function browseEffectiveKnowledge(db, input) {
1453
2167
  topic,
1454
2168
  sourceKinds
1455
2169
  };
1456
- const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
1457
- if (parent && after > 2147483648n) {
1458
- throw new Error("invalid knowledge browse cursor");
1459
- }
1460
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
2170
+ const topLevelAfter = !parent && input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
2171
+ const access = await resolveEffectiveDocumentAccess(db, {
2172
+ accountId: input.accountId,
2173
+ workspaceId: input.workspaceId,
2174
+ initiatingSubjectId,
2175
+ surface: input.surface ?? "agent",
2176
+ agentAuthority: input.agentAuthority
2177
+ });
1461
2178
  return await withDocumentAccountRls(
1462
2179
  db,
1463
2180
  input.accountId,
@@ -1465,7 +2182,7 @@ async function browseEffectiveKnowledge(db, input) {
1465
2182
  access,
1466
2183
  async (scopedDb) => {
1467
2184
  if (parent) {
1468
- const [authorizedParent] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
2185
+ const [authorizedParent] = await scopedDb.select({ id: schema.documents.id, indexSequence: schema.documents.indexSequence }).from(schema.documents).where(
1469
2186
  and(
1470
2187
  eq(schema.documents.accountId, input.accountId),
1471
2188
  eq(schema.documents.id, parent.id),
@@ -1473,34 +2190,57 @@ async function browseEffectiveKnowledge(db, input) {
1473
2190
  ...documentAccessConditions(input.workspaceId, access)
1474
2191
  )
1475
2192
  ).limit(1);
1476
- if (!authorizedParent) return { records: [], nextCursor: null, hasMore: false };
2193
+ if (!authorizedParent) {
2194
+ return selectKnowledgeBrowseRecords({ entries: [], hasMoreAfterEntries: false });
2195
+ }
2196
+ if (authorizedParent.indexSequence === null) {
2197
+ throw new Error("ready knowledge document is missing its index revision");
2198
+ }
2199
+ const parentCursorScope = {
2200
+ ...cursorScope,
2201
+ parentRevision: authorizedParent.indexSequence.toString()
2202
+ };
2203
+ const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, parentCursorScope) : 0n;
2204
+ if (after > 2147483648n) {
2205
+ throw new Error("invalid knowledge browse cursor");
2206
+ }
1477
2207
  const rows2 = await scopedDb.select({
1478
2208
  chunk: schema.documentChunks,
1479
2209
  document: schema.documents,
1480
- citation: googleDriveCitationProjection(input.workspaceId, access)
2210
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2211
+ previousChunkId: knowledgePreviousChunkIdProjection(),
2212
+ nextChunkId: knowledgeNextChunkIdProjection()
1481
2213
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1482
2214
  and(
1483
2215
  eq(schema.documentChunks.accountId, input.accountId),
1484
2216
  eq(schema.documentChunks.documentId, parent.id),
1485
2217
  gt(schema.documentChunks.chunkIndex, Number(after) - 1),
1486
2218
  eq(schema.documents.status, "ready"),
2219
+ eq(schema.documents.indexSequence, authorizedParent.indexSequence),
1487
2220
  ...documentAccessConditions(input.workspaceId, access)
1488
2221
  )
1489
2222
  ).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
1490
2223
  const hasMore2 = rows2.length > limit;
1491
2224
  const page2 = rows2.slice(0, limit);
1492
- const last2 = page2.at(-1)?.chunk.chunkIndex;
1493
- return {
1494
- records: page2.map((row) => knowledgeChunkRecord(row.document, row.chunk, row.citation)),
1495
- nextCursor: hasMore2 && last2 !== void 0 ? encodeKnowledgeBrowseCursor(cursorScope, BigInt(last2 + 1)) : null,
1496
- hasMore: hasMore2
1497
- };
2225
+ return selectKnowledgeBrowseRecords({
2226
+ entries: page2.map((row) => ({
2227
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
2228
+ previousChunkId: row.previousChunkId,
2229
+ nextChunkId: row.nextChunkId
2230
+ }),
2231
+ cursorAfter: encodeKnowledgeBrowseCursor(
2232
+ parentCursorScope,
2233
+ BigInt(row.chunk.chunkIndex + 1)
2234
+ )
2235
+ })),
2236
+ hasMoreAfterEntries: hasMore2
2237
+ });
1498
2238
  }
1499
2239
  const conditions = [
1500
2240
  eq(schema.documents.accountId, input.accountId),
1501
2241
  eq(schema.documents.status, "ready"),
1502
2242
  isNotNull(schema.documents.indexSequence),
1503
- gt(schema.documents.indexSequence, after),
2243
+ gt(schema.documents.indexSequence, topLevelAfter),
1504
2244
  ...documentAccessConditions(input.workspaceId, access)
1505
2245
  ];
1506
2246
  if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
@@ -1508,23 +2248,38 @@ async function browseEffectiveKnowledge(db, input) {
1508
2248
  conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
1509
2249
  const rows = await scopedDb.select({
1510
2250
  document: schema.documents,
1511
- citation: googleDriveCitationProjection(input.workspaceId, access)
1512
- }).from(schema.documents).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
2251
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2252
+ firstChunkId: schema.documentChunks.id
2253
+ }).from(schema.documents).leftJoin(
2254
+ schema.documentChunks,
2255
+ and(
2256
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
2257
+ eq(schema.documentChunks.documentId, schema.documents.id),
2258
+ eq(schema.documentChunks.chunkIndex, 0)
2259
+ )
2260
+ ).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
1513
2261
  const hasMore = rows.length > limit;
1514
2262
  const page = rows.slice(0, limit);
1515
- const last = page.at(-1)?.document.indexSequence;
1516
- return {
1517
- records: page.map((row) => knowledgeDocumentRecord(row.document, row.citation)),
1518
- nextCursor: hasMore && last !== void 0 && last !== null ? encodeKnowledgeBrowseCursor(cursorScope, last) : null,
1519
- hasMore
1520
- };
2263
+ return selectKnowledgeBrowseRecords({
2264
+ entries: page.map((row) => {
2265
+ if (row.document.indexSequence === null) {
2266
+ throw new Error("ready knowledge document is missing its index revision");
2267
+ }
2268
+ return {
2269
+ record: knowledgeDocumentRecord(row.document, row.citation, row.firstChunkId),
2270
+ cursorAfter: encodeKnowledgeBrowseCursor(cursorScope, row.document.indexSequence)
2271
+ };
2272
+ }),
2273
+ hasMoreAfterEntries: hasMore
2274
+ });
1521
2275
  }
1522
2276
  );
1523
2277
  }
1524
2278
  function encodeKnowledgeBrowseCursor(scope, position) {
1525
2279
  if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
2280
+ const version = knowledgeBrowseCursorVersion(scope);
1526
2281
  return Buffer.from(
1527
- JSON.stringify({ v: 1, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
2282
+ JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
1528
2283
  "utf8"
1529
2284
  ).toString("base64url");
1530
2285
  }
@@ -1536,7 +2291,8 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1536
2291
  const bytes = Buffer.from(value, "base64url");
1537
2292
  if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1538
2293
  const parsed = JSON.parse(bytes.toString("utf8"));
1539
- if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== 1 || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
2294
+ const expectedVersion = knowledgeBrowseCursorVersion(scope);
2295
+ if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== expectedVersion || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
1540
2296
  throw new Error("cursor payload");
1541
2297
  }
1542
2298
  if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
@@ -1553,7 +2309,17 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1553
2309
  }
1554
2310
  }
1555
2311
  function knowledgeBrowseCursorScope(scope) {
1556
- return createHash("sha256").update("opengeni:knowledge-browse-cursor:v1\0").update(scope.accountId).update("\0").update(scope.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(scope.initiatingSubjectId)).update("\0").update(scope.parentId ?? "").update("\0").update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
2312
+ const version = knowledgeBrowseCursorVersion(scope);
2313
+ const hash = createHash("sha256").update(`opengeni:knowledge-browse-cursor:v${version}\0`).update(scope.accountId).update("\0").update(scope.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(scope.initiatingSubjectId)).update("\0").update(scope.parentId ?? "").update("\0");
2314
+ if (version === 2) hash.update(scope.parentRevision).update("\0");
2315
+ return hash.update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
2316
+ }
2317
+ function knowledgeBrowseCursorVersion(scope) {
2318
+ if (!scope.parentId) return 1;
2319
+ if (!scope.parentRevision || !/^[1-9][0-9]*$/.test(scope.parentRevision)) {
2320
+ throw new Error("knowledge browse parent cursor requires an exact document revision");
2321
+ }
2322
+ return 2;
1557
2323
  }
1558
2324
  function parseKnowledgeRecordId(value) {
1559
2325
  const match = /^(document|document_chunk):([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/iu.exec(
@@ -1562,7 +2328,7 @@ function parseKnowledgeRecordId(value) {
1562
2328
  if (!match) throw new Error("invalid knowledge record id");
1563
2329
  return { kind: match[1], id: match[2].toLowerCase() };
1564
2330
  }
1565
- function knowledgeDocumentRecord(document, citation = null) {
2331
+ function knowledgeDocumentRecord(document, citation = null, firstChunkId = null) {
1566
2332
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1567
2333
  const projected = projectKnowledgeRecord({
1568
2334
  title: document.title,
@@ -1585,11 +2351,22 @@ function knowledgeDocumentRecord(document, citation = null) {
1585
2351
  },
1586
2352
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1587
2353
  quality: knowledgeQuality(document),
1588
- links: knowledgeSourceLinks(projected.source.uri),
2354
+ links: [
2355
+ ...firstChunkId ? [
2356
+ {
2357
+ relation: "contents",
2358
+ target: {
2359
+ kind: "knowledge",
2360
+ id: `document_chunk:${firstChunkId}`
2361
+ }
2362
+ }
2363
+ ] : [],
2364
+ ...knowledgeSourceLinks(projected.source.uri)
2365
+ ],
1589
2366
  projection: projected.projection
1590
2367
  };
1591
2368
  }
1592
- function knowledgeChunkRecord(document, chunk, citation = null) {
2369
+ function knowledgeChunkRecord(document, chunk, citation = null, traversal = { previousChunkId: null, nextChunkId: null }) {
1593
2370
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1594
2371
  const projected = projectKnowledgeRecord({
1595
2372
  title: document.title,
@@ -1613,7 +2390,28 @@ function knowledgeChunkRecord(document, chunk, citation = null) {
1613
2390
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1614
2391
  quality: knowledgeQuality(document),
1615
2392
  links: [
1616
- { relation: "parent", target: { kind: "knowledge", id: `document:${document.id}` } },
2393
+ {
2394
+ relation: "parent",
2395
+ target: { kind: "knowledge", id: `document:${document.id}` }
2396
+ },
2397
+ ...traversal.previousChunkId ? [
2398
+ {
2399
+ relation: "previous",
2400
+ target: {
2401
+ kind: "knowledge",
2402
+ id: `document_chunk:${traversal.previousChunkId}`
2403
+ }
2404
+ }
2405
+ ] : [],
2406
+ ...traversal.nextChunkId ? [
2407
+ {
2408
+ relation: "next",
2409
+ target: {
2410
+ kind: "knowledge",
2411
+ id: `document_chunk:${traversal.nextChunkId}`
2412
+ }
2413
+ }
2414
+ ] : [],
1617
2415
  ...knowledgeSourceLinks(projected.source.uri)
1618
2416
  ],
1619
2417
  projection: projected.projection
@@ -1643,6 +2441,28 @@ function knowledgeQuality(document) {
1643
2441
  function knowledgeSourceLinks(sourceUri) {
1644
2442
  return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
1645
2443
  }
2444
+ function knowledgePreviousChunkIdProjection() {
2445
+ return sql`(
2446
+ select knowledge_previous_chunk.id
2447
+ from document_chunks knowledge_previous_chunk
2448
+ where knowledge_previous_chunk.account_id = ${schema.documentChunks.accountId}
2449
+ and knowledge_previous_chunk.document_id = ${schema.documentChunks.documentId}
2450
+ and knowledge_previous_chunk.chunk_index < ${schema.documentChunks.chunkIndex}
2451
+ order by knowledge_previous_chunk.chunk_index desc
2452
+ limit 1
2453
+ )`;
2454
+ }
2455
+ function knowledgeNextChunkIdProjection() {
2456
+ return sql`(
2457
+ select knowledge_next_chunk.id
2458
+ from document_chunks knowledge_next_chunk
2459
+ where knowledge_next_chunk.account_id = ${schema.documentChunks.accountId}
2460
+ and knowledge_next_chunk.document_id = ${schema.documentChunks.documentId}
2461
+ and knowledge_next_chunk.chunk_index > ${schema.documentChunks.chunkIndex}
2462
+ order by knowledge_next_chunk.chunk_index asc
2463
+ limit 1
2464
+ )`;
2465
+ }
1646
2466
  async function vectorSearchDocuments(db, input, limit, services) {
1647
2467
  const queryEmbedding = await services.embedder.embedQuery(input.query);
1648
2468
  validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
@@ -1676,7 +2496,7 @@ async function vectorSearchDocuments(db, input, limit, services) {
1676
2496
  authoritySubjectId: schema.documents.authoritySubjectId,
1677
2497
  citation: googleDriveCitationProjection(input.workspaceId, input.access),
1678
2498
  distance
1679
- }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance).limit(limit)
2499
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance, asc(schema.documentChunks.id)).limit(limit)
1680
2500
  );
1681
2501
  return rows.map((row) => ({
1682
2502
  ...mapSearchRowBase(row),
@@ -1720,7 +2540,7 @@ async function keywordSearchDocuments(db, input, limit) {
1720
2540
  ...documentSearchConditions(input),
1721
2541
  sql`to_tsvector('simple', ${schema.documentChunks.text}) @@ plainto_tsquery('simple', ${input.query})`
1722
2542
  )
1723
- ).orderBy(desc(rank)).limit(limit)
2543
+ ).orderBy(desc(rank), asc(schema.documentChunks.id)).limit(limit)
1724
2544
  );
1725
2545
  return rows.map((row) => ({
1726
2546
  ...mapSearchRowBase(row),
@@ -1816,6 +2636,21 @@ async function assertDocumentAccountWorkspace(db, accountId, workspaceId) {
1816
2636
  }
1817
2637
  return context;
1818
2638
  }
2639
+ async function resolveEffectiveDocumentAccess(_db, input) {
2640
+ if (input.surface === "human") {
2641
+ return { viewerSubjectId: input.initiatingSubjectId };
2642
+ }
2643
+ return {
2644
+ agentOnly: true,
2645
+ viewerSubjectId: input.initiatingSubjectId,
2646
+ authorizedPersonalAttempt: input.agentAuthority ? {
2647
+ accountId: input.accountId,
2648
+ workspaceId: input.workspaceId,
2649
+ sessionId: input.agentAuthority.sessionId,
2650
+ attemptId: input.agentAuthority.attemptId
2651
+ } : void 0
2652
+ };
2653
+ }
1819
2654
  function documentAccessConditions(workspaceId, access) {
1820
2655
  const organization = eq(schema.documents.authorityKind, "organization");
1821
2656
  const workspace = and(
@@ -1823,10 +2658,28 @@ function documentAccessConditions(workspaceId, access) {
1823
2658
  eq(schema.documents.authorityWorkspaceId, workspaceId)
1824
2659
  );
1825
2660
  const viewer = cleanString(access?.viewerSubjectId ?? null);
2661
+ const personalAttempt = access?.authorizedPersonalAttempt;
2662
+ const authorizedPersonal = personalAttempt ? sql`${schema.documents.id} IN (
2663
+ SELECT resolve_session_attempt_personal_document_reads(
2664
+ ${personalAttempt.accountId}::uuid,
2665
+ ${personalAttempt.workspaceId}::uuid,
2666
+ ${personalAttempt.sessionId}::uuid,
2667
+ ${personalAttempt.attemptId}::uuid
2668
+ )
2669
+ )` : sql`false`;
1826
2670
  const personal = viewer ? and(
1827
2671
  eq(schema.documents.authorityKind, "personal"),
1828
- eq(schema.documents.authorityWorkspaceId, workspaceId),
1829
- eq(schema.documents.authoritySubjectId, viewer)
2672
+ eq(schema.documents.authoritySubjectId, viewer),
2673
+ access?.agentOnly ? or(
2674
+ and(
2675
+ isNull(schema.documents.authorityId),
2676
+ eq(schema.documents.authorityWorkspaceId, workspaceId)
2677
+ ),
2678
+ and(isNotNull(schema.documents.authorityId), authorizedPersonal)
2679
+ ) ?? authorizedPersonal : or(
2680
+ eq(schema.documents.authorityWorkspaceId, workspaceId),
2681
+ isNull(schema.documents.authorityWorkspaceId)
2682
+ ) ?? eq(schema.documents.authorityWorkspaceId, workspaceId)
1830
2683
  ) : void 0;
1831
2684
  const authority = viewer ? or(organization, workspace, personal) ?? organization : or(organization, workspace) ?? organization;
1832
2685
  const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
@@ -1843,7 +2696,7 @@ function documentAccessConditions(workspaceId, access) {
1843
2696
  }
1844
2697
  function documentMatchesAccess(document, workspaceId, access) {
1845
2698
  if (access?.agentOnly) {
1846
- return document.agentAccess && canViewDocument(document, access.viewerSubjectId, workspaceId);
2699
+ return document.agentAccess && (document.authorityKind !== "personal" || document.authorityId === null) && canViewDocument(document, access.viewerSubjectId, workspaceId);
1847
2700
  }
1848
2701
  return canViewDocument(document, access?.viewerSubjectId, workspaceId);
1849
2702
  }
@@ -1861,16 +2714,16 @@ function canViewDocument(document, viewerSubjectId, workspaceId) {
1861
2714
  return document.authorityWorkspaceId === null && document.authoritySubjectId === null;
1862
2715
  }
1863
2716
  const normalizedWorkspaceId = cleanString(workspaceId ?? null);
1864
- if (!normalizedWorkspaceId || document.authorityWorkspaceId !== normalizedWorkspaceId) {
2717
+ if (!normalizedWorkspaceId) {
1865
2718
  return false;
1866
2719
  }
1867
2720
  if (document.authorityKind === "workspace") {
1868
- return document.authoritySubjectId === null;
2721
+ return document.authorityWorkspaceId === normalizedWorkspaceId && document.authoritySubjectId === null;
1869
2722
  }
1870
2723
  if (document.authorityKind === "personal") {
1871
2724
  const authoritySubjectId = canonicalDocumentAuthoritySubject(document.authoritySubjectId);
1872
2725
  const viewer = canonicalDocumentAuthoritySubject(viewerSubjectId);
1873
- return !!authoritySubjectId && authoritySubjectId === viewer;
2726
+ return !!authoritySubjectId && authoritySubjectId === viewer && (document.authorityWorkspaceId === null || document.authorityWorkspaceId === normalizedWorkspaceId);
1874
2727
  }
1875
2728
  return false;
1876
2729
  }
@@ -1960,7 +2813,7 @@ function mergeDocumentSearchRows(rows, mode) {
1960
2813
  matchType
1961
2814
  };
1962
2815
  }).sort(
1963
- (left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex
2816
+ (left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex || (left.chunkId === right.chunkId ? 0 : left.chunkId < right.chunkId ? -1 : 1)
1964
2817
  );
1965
2818
  }
1966
2819
  function combinedSearchScore(mode, vectorScore, keywordScore, matchType) {
@@ -2147,6 +3000,7 @@ function mapDocument(row) {
2147
3000
  authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
2148
3001
  authorityWorkspaceId: row.authorityWorkspaceId,
2149
3002
  authoritySubjectId: row.authoritySubjectId,
3003
+ authorityId: row.authorityId,
2150
3004
  visibility: normalizeDocumentVisibility(row.visibility),
2151
3005
  createdBy: row.createdBy,
2152
3006
  agentAccess: row.agentAccess,
@@ -2251,23 +3105,35 @@ export {
2251
3105
  getDocument,
2252
3106
  getDocumentBase,
2253
3107
  getDocumentChunk,
3108
+ getDocumentDefaultCollectionBackfillAudit,
2254
3109
  getDocumentForIndexing,
2255
3110
  getDocumentInventory,
3111
+ getDocumentOriginalFile,
2256
3112
  getEffectiveKnowledgeRecord,
2257
3113
  heuristicCuration,
2258
3114
  indexDocumentNow,
3115
+ listAccessibleDocuments,
3116
+ listDocumentAuthorityReclassifications,
2259
3117
  listDocumentBases,
2260
3118
  listDocumentBasesEnsuringDefault,
3119
+ listDocumentDefaultCollectionBackfillRuns,
2261
3120
  listDocuments,
2262
3121
  listEffectiveIndexedDocuments,
3122
+ listOrganizationDocumentAuthorityReclassifications,
2263
3123
  moveDocumentToBase,
2264
3124
  parseCurationOutcome,
2265
3125
  parseDocumentBytes,
2266
3126
  projectKnowledgeRecord,
2267
3127
  queueDocumentForReindex,
3128
+ reclassifyDocumentAuthority,
2268
3129
  resolveDocumentAuthority,
3130
+ resolveEffectiveDocumentAccess,
3131
+ runDocumentDefaultCollectionBackfill,
2269
3132
  searchDocuments,
2270
3133
  searchEffectiveDocuments,
2271
- searchEffectiveKnowledge
3134
+ searchEffectiveKnowledge,
3135
+ selectDocumentSearchCandidateWindow,
3136
+ selectKnowledgeBrowseRecords,
3137
+ selectKnowledgeSearchResults
2272
3138
  };
2273
3139
  //# sourceMappingURL=index.js.map