@opengeni/documents 0.5.41 → 0.6.9-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,14 @@
1
1
  // src/index.ts
2
2
  import {
3
- KnowledgeProviderCitation
3
+ KnowledgeProviderCitation,
4
+ DocumentAuthorityReclassification,
5
+ ListDocumentAuthorityReclassificationsResponse,
6
+ DocumentDefaultCollectionBackfill
4
7
  } from "@opengeni/contracts";
5
8
  import {
9
+ createPersonalDocumentAuthority,
6
10
  getFilesForSubject,
11
+ resolveDocumentOriginalFileForSubject,
7
12
  rlsContextForWorkspace,
8
13
  setSubjectRlsContext,
9
14
  withRlsContext,
@@ -11,9 +16,20 @@ import {
11
16
  withWorkspaceSubjectRls
12
17
  } from "@opengeni/db";
13
18
  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";
19
+ import { retryWhileMissing } from "@opengeni/storage";
20
+ import { createHash, randomUUID } from "crypto";
21
+ import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
22
+ import {
23
+ KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS,
24
+ KNOWLEDGE_BROWSE_MAX_LIMIT,
25
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
26
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
27
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS,
28
+ KNOWLEDGE_SEARCH_MAX_RESULTS,
29
+ KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE,
30
+ KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
31
+ KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
32
+ } from "@opengeni/contracts";
17
33
 
18
34
  // src/knowledge-projection.ts
19
35
  import {
@@ -697,6 +713,135 @@ async function listDocumentBasesEnsuringDefault(db, input) {
697
713
  await ensureDefaultBase(db, input);
698
714
  return await listDocumentBases(db, input.workspaceId);
699
715
  }
716
+ async function runDocumentDefaultCollectionBackfill(db, input) {
717
+ return await withDocumentAccountRls(
718
+ db,
719
+ input.accountId,
720
+ input.workspaceId,
721
+ { viewerSubjectId: input.actorSubjectId },
722
+ async (scopedDb) => {
723
+ const command = {
724
+ accountId: input.accountId,
725
+ workspaceId: input.workspaceId,
726
+ actorSubjectId: input.actorSubjectId,
727
+ runId: input.runId,
728
+ operationId: input.operationId,
729
+ batchSize: input.batchSize,
730
+ accountAdminAuthorization: input.accountAdminAuthorization
731
+ };
732
+ const rows = await scopedDb.execute(sql`
733
+ SELECT run_document_default_collection_backfill(
734
+ ${JSON.stringify(command)}::jsonb
735
+ ) AS result
736
+ `);
737
+ const row = rows[0];
738
+ if (!row) throw new Error("Document Default collection backfill returned no result");
739
+ return DocumentDefaultCollectionBackfill.parse(row.result);
740
+ }
741
+ );
742
+ }
743
+ async function reclassifyDocumentAuthority(db, input) {
744
+ return await withDocumentAccountRls(
745
+ db,
746
+ input.accountId,
747
+ input.workspaceId,
748
+ { viewerSubjectId: input.actorSubjectId },
749
+ async (scopedDb) => {
750
+ const command = {
751
+ accountId: input.accountId,
752
+ workspaceId: input.workspaceId,
753
+ documentId: input.documentId,
754
+ operationId: input.operationId,
755
+ actorSubjectId: input.actorSubjectId,
756
+ expectedAuthority: input.expectedAuthority,
757
+ targetAuthorityKind: input.targetAuthorityKind,
758
+ accountAdminAuthorization: input.accountAdminAuthorization
759
+ };
760
+ const rows = await scopedDb.execute(sql`
761
+ SELECT reclassify_document_authority(${JSON.stringify(command)}::jsonb) AS result
762
+ `);
763
+ const row = rows[0];
764
+ if (!row) throw new Error("Document authority reclassification returned no result");
765
+ return DocumentAuthorityReclassification.parse(row.result);
766
+ }
767
+ );
768
+ }
769
+ async function listDocumentAuthorityReclassifications(db, input) {
770
+ const limit = input.limit ?? 50;
771
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
772
+ throw new Error("document authority receipt limit must be between 1 and 100");
773
+ }
774
+ const cursor = input.cursor ? decodeDocumentAuthorityReclassificationCursor(input.cursor, input) : null;
775
+ return await withDocumentAccountRls(
776
+ db,
777
+ input.accountId,
778
+ input.workspaceId,
779
+ { viewerSubjectId: input.actorSubjectId },
780
+ async (scopedDb) => {
781
+ const rows = await scopedDb.execute(sql`
782
+ SELECT list_document_authority_reclassifications(
783
+ ${input.accountId}::uuid,
784
+ ${input.workspaceId}::uuid,
785
+ ${input.actorSubjectId},
786
+ ${input.documentId}::uuid,
787
+ ${limit + 1}::integer,
788
+ ${cursor?.createdAt ?? null}::timestamptz,
789
+ ${cursor?.operationId ?? null}::uuid
790
+ ) AS result
791
+ `);
792
+ const parsed = rows.map((row) => DocumentAuthorityReclassification.parse(row.result));
793
+ const hasMore = parsed.length > limit;
794
+ const receipts = parsed.slice(0, limit);
795
+ const tail = hasMore ? receipts.at(-1) : null;
796
+ return ListDocumentAuthorityReclassificationsResponse.parse({
797
+ receipts,
798
+ hasMore,
799
+ nextCursor: tail ? encodeDocumentAuthorityReclassificationCursor(input, {
800
+ createdAt: tail.createdAt,
801
+ operationId: tail.operationId
802
+ }) : null
803
+ });
804
+ }
805
+ );
806
+ }
807
+ function documentAuthorityReclassificationCursorScope(input) {
808
+ return createHash("sha256").update(
809
+ JSON.stringify([
810
+ "document_authority_reclassification_cursor",
811
+ 1,
812
+ input.accountId,
813
+ input.workspaceId,
814
+ input.documentId,
815
+ input.actorSubjectId
816
+ ]),
817
+ "utf8"
818
+ ).digest("hex").slice(0, 32);
819
+ }
820
+ function encodeDocumentAuthorityReclassificationCursor(scope, cursor) {
821
+ return Buffer.from(
822
+ JSON.stringify({
823
+ v: 1,
824
+ s: documentAuthorityReclassificationCursorScope(scope),
825
+ t: cursor.createdAt,
826
+ i: cursor.operationId
827
+ }),
828
+ "utf8"
829
+ ).toString("base64url");
830
+ }
831
+ function decodeDocumentAuthorityReclassificationCursor(value, scope) {
832
+ try {
833
+ if (!value || value.length > 1024) throw new Error("cursor length");
834
+ const bytes = Buffer.from(value, "base64url");
835
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
836
+ const parsed = JSON.parse(bytes.toString("utf8"));
837
+ 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)) {
838
+ throw new Error("cursor payload");
839
+ }
840
+ return { createdAt: parsed.t, operationId: parsed.i };
841
+ } catch (error) {
842
+ throw new Error("invalid document authority receipt cursor", { cause: error });
843
+ }
844
+ }
700
845
  async function addDocumentToBase(db, input) {
701
846
  return await withRlsContext(
702
847
  db,
@@ -776,33 +921,47 @@ async function addDocumentToBase(db, input) {
776
921
  ).returning();
777
922
  return mapDocument(updated ?? existing);
778
923
  }
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();
924
+ const documentId = randomUUID();
925
+ const row = await scopedDb.transaction(async (tx) => {
926
+ const userAuthority = authority.kind === "personal" ? await createPersonalDocumentAuthority(tx, {
927
+ accountId: input.accountId,
928
+ workspaceId: input.workspaceId,
929
+ subjectId: authority.subjectId,
930
+ documentId
931
+ }) : null;
932
+ const [inserted] = await tx.insert(schema.documents).values({
933
+ id: documentId,
934
+ accountId: input.accountId,
935
+ workspaceId: input.workspaceId,
936
+ baseId: input.baseId,
937
+ fileId: input.fileId,
938
+ status: "queued",
939
+ title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
940
+ parser: DEFAULT_DOCUMENT_PARSER,
941
+ sourceKind: input.sourceKind ?? "manual_upload",
942
+ sourceUri: cleanString(input.sourceUri) ?? null,
943
+ sourceExternalId: cleanString(input.sourceExternalId) ?? null,
944
+ sourceTitle: cleanString(input.sourceTitle) ?? null,
945
+ sourceAuthor: cleanString(input.sourceAuthor) ?? null,
946
+ sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
947
+ sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
948
+ sourceVersion: cleanString(input.sourceVersion) ?? null,
949
+ knowledgeSourceIdentity,
950
+ aclTags: cleanStringArray(input.aclTags),
951
+ authorityKind: authority.kind,
952
+ authorityWorkspaceId: userAuthority ? null : authority.workspaceId,
953
+ authoritySubjectId: authority.subjectId,
954
+ authorityId: userAuthority?.authorityId ?? null,
955
+ ownerOrganizationMembershipId: userAuthority?.ownerOrganizationMembershipId ?? null,
956
+ originWorkspaceId: input.workspaceId,
957
+ visibility: authority.kind === "personal" ? "private" : "workspace",
958
+ agentAccess: input.agentAccess ?? true,
959
+ createdBy: fileAuthoritySubjectId,
960
+ curationStatus: input.curationStatus ?? "none",
961
+ updatedAt: now
962
+ }).returning();
963
+ return inserted;
964
+ });
806
965
  if (!row) throw new Error("Failed to create document");
807
966
  return mapDocument(row);
808
967
  }
@@ -817,7 +976,7 @@ async function moveDocumentToBase(db, input) {
817
976
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
818
977
  const [row] = await scopedDb.select().from(schema.documents).where(
819
978
  and(
820
- eq(schema.documents.workspaceId, input.workspaceId),
979
+ eq(schema.documents.accountId, input.accountId),
821
980
  eq(schema.documents.id, input.documentId),
822
981
  ...documentAccessConditions(input.workspaceId, input.access)
823
982
  )
@@ -830,11 +989,11 @@ async function moveDocumentToBase(db, input) {
830
989
  throw new Error("document has no suggested base; pass targetBaseId");
831
990
  }
832
991
  if (targetBaseId === row.baseId) return mapDocument(row);
833
- const base = await getDocumentBase(scopedDb, input.workspaceId, targetBaseId);
992
+ const base = await getDocumentBase(scopedDb, row.workspaceId, targetBaseId);
834
993
  if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
835
994
  const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
836
995
  and(
837
- eq(schema.documents.workspaceId, input.workspaceId),
996
+ eq(schema.documents.workspaceId, row.workspaceId),
838
997
  eq(schema.documents.baseId, targetBaseId),
839
998
  ...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
840
999
  )
@@ -850,7 +1009,7 @@ async function moveDocumentToBase(db, input) {
850
1009
  updatedAt: now
851
1010
  }).where(
852
1011
  and(
853
- eq(schema.documents.workspaceId, input.workspaceId),
1012
+ eq(schema.documents.workspaceId, row.workspaceId),
854
1013
  eq(schema.documents.id, input.documentId),
855
1014
  ...documentAccessConditions(input.workspaceId, input.access)
856
1015
  )
@@ -858,7 +1017,7 @@ async function moveDocumentToBase(db, input) {
858
1017
  if (updated) {
859
1018
  await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
860
1019
  and(
861
- eq(schema.documentChunks.workspaceId, input.workspaceId),
1020
+ eq(schema.documentChunks.workspaceId, row.workspaceId),
862
1021
  eq(schema.documentChunks.documentId, input.documentId)
863
1022
  )
864
1023
  );
@@ -879,7 +1038,7 @@ async function deleteDocumentFromBase(db, input) {
879
1038
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
880
1039
  const [document] = await scopedDb.select().from(schema.documents).where(
881
1040
  and(
882
- eq(schema.documents.workspaceId, input.workspaceId),
1041
+ eq(schema.documents.accountId, input.accountId),
883
1042
  eq(schema.documents.id, input.documentId),
884
1043
  ...documentAccessConditions(input.workspaceId, input.access)
885
1044
  )
@@ -896,7 +1055,7 @@ async function deleteDocumentFromBase(db, input) {
896
1055
  }
897
1056
  await scopedDb.delete(schema.documents).where(
898
1057
  and(
899
- eq(schema.documents.workspaceId, input.workspaceId),
1058
+ eq(schema.documents.accountId, input.accountId),
900
1059
  eq(schema.documents.id, input.documentId),
901
1060
  ...documentAccessConditions(input.workspaceId, input.access)
902
1061
  )
@@ -916,6 +1075,12 @@ async function listDocuments(db, workspaceId, baseId, access) {
916
1075
  return rows.map(mapDocument);
917
1076
  });
918
1077
  }
1078
+ async function listAccessibleDocuments(db, workspaceId, access) {
1079
+ return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1080
+ const rows = await scopedDb.select().from(schema.documents).where(and(...documentAccessConditions(workspaceId, access))).orderBy(desc(schema.documents.updatedAt), asc(schema.documents.createdAt));
1081
+ return rows.map(mapDocument);
1082
+ });
1083
+ }
919
1084
  async function listEffectiveIndexedDocuments(db, input) {
920
1085
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
921
1086
  const limit = input.limit ?? 50;
@@ -927,10 +1092,13 @@ async function listEffectiveIndexedDocuments(db, input) {
927
1092
  workspaceId: input.workspaceId,
928
1093
  initiatingSubjectId
929
1094
  }) : 0n;
930
- const access = {
931
- agentOnly: true,
932
- viewerSubjectId: initiatingSubjectId
933
- };
1095
+ const access = await resolveEffectiveDocumentAccess(db, {
1096
+ accountId: input.accountId,
1097
+ workspaceId: input.workspaceId,
1098
+ initiatingSubjectId,
1099
+ surface: "agent",
1100
+ agentAuthority: input.agentAuthority
1101
+ });
934
1102
  const rows = await withDocumentAccountRls(
935
1103
  db,
936
1104
  input.accountId,
@@ -1014,15 +1182,21 @@ function canonicalEffectiveDocumentSubject(value) {
1014
1182
  async function getDocument(db, workspaceId, documentId, access) {
1015
1183
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1016
1184
  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
- )
1185
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1022
1186
  ).limit(1);
1023
1187
  return row ? mapDocument(row) : null;
1024
1188
  });
1025
1189
  }
1190
+ async function getDocumentOriginalFile(db, input) {
1191
+ const subjectId = cleanString(input.access.viewerSubjectId ?? null);
1192
+ if (!subjectId || input.access.agentOnly) return null;
1193
+ return await resolveDocumentOriginalFileForSubject(db, {
1194
+ accountId: input.accountId,
1195
+ workspaceId: input.workspaceId,
1196
+ subjectId,
1197
+ documentId: input.documentId
1198
+ });
1199
+ }
1026
1200
  async function getDocumentForIndexing(db, workspaceId, documentId) {
1027
1201
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1028
1202
  const [row] = await scopedDb.select().from(schema.documents).where(
@@ -1033,12 +1207,8 @@ async function getDocumentForIndexing(db, workspaceId, documentId) {
1033
1207
  }
1034
1208
  async function queueDocumentForReindex(db, workspaceId, documentId, access, organizationAuthorityGranted) {
1035
1209
  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
- )
1210
+ const [document] = await scopedDb.select().from(schema.documents).where(
1211
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1042
1212
  ).limit(1);
1043
1213
  if (!document) throw new Error(`Document not found: ${documentId}`);
1044
1214
  assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
@@ -1047,11 +1217,7 @@ async function queueDocumentForReindex(db, workspaceId, documentId, access, orga
1047
1217
  error: null,
1048
1218
  updatedAt: /* @__PURE__ */ new Date()
1049
1219
  }).where(
1050
- and(
1051
- eq(schema.documents.workspaceId, workspaceId),
1052
- eq(schema.documents.id, documentId),
1053
- ...documentAccessConditions(workspaceId, access)
1054
- )
1220
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1055
1221
  ).returning();
1056
1222
  if (!row) throw new Error(`Document not found: ${documentId}`);
1057
1223
  return mapDocument(row);
@@ -1090,7 +1256,11 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1090
1256
  );
1091
1257
  });
1092
1258
  try {
1093
- const bytes = await objectStorage.getFileBytes(file);
1259
+ const object = await retryWhileMissing(
1260
+ async () => objectStorage.getObjectBytes(file.objectKey)
1261
+ );
1262
+ if (!object) throw new Error("document source object is missing");
1263
+ const bytes = object.bytes;
1094
1264
  const parsed = await services.parser.parse(bytes, file);
1095
1265
  if (document.curationStatus === "pending") {
1096
1266
  document = await curateDroppedDocument(db, services, document, parsed, file);
@@ -1284,6 +1454,9 @@ async function curateDroppedDocument(db, services, document, parsed, file) {
1284
1454
  return updated ?? document;
1285
1455
  }
1286
1456
  async function searchDocuments(db, input, services = createDocumentServices()) {
1457
+ return (await searchDocumentCandidates(db, input, services)).results;
1458
+ }
1459
+ async function searchDocumentCandidates(db, input, services, relevanceFloor) {
1287
1460
  await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
1288
1461
  const mode = input.mode ?? "hybrid";
1289
1462
  const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
@@ -1308,13 +1481,34 @@ async function searchDocuments(db, input, services = createDocumentServices()) {
1308
1481
  if (mode === "keyword" || mode === "hybrid") {
1309
1482
  rows.push(...await keywordSearchDocuments(db, input, candidateLimit));
1310
1483
  }
1311
- return mergeDocumentSearchRows(rows, mode).slice(0, limit);
1484
+ const merged = mergeDocumentSearchRows(rows, mode);
1485
+ return selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor);
1486
+ }
1487
+ function selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor) {
1488
+ const boundedLimit = Math.min(Math.max(Math.trunc(limit), 1), 50);
1489
+ if (!relevanceFloor) {
1490
+ return { results: merged.slice(0, boundedLimit), belowRelevanceFloor: 0 };
1491
+ }
1492
+ let belowRelevanceFloor = 0;
1493
+ const relevant = merged.filter((result) => {
1494
+ const included = result.vectorScore !== null && result.vectorScore >= relevanceFloor.vectorScore || result.keywordScore !== null && result.keywordScore >= relevanceFloor.keywordScore;
1495
+ if (!included) belowRelevanceFloor += 1;
1496
+ return included;
1497
+ });
1498
+ return { results: relevant.slice(0, boundedLimit), belowRelevanceFloor };
1312
1499
  }
1313
1500
  async function searchEffectiveDocuments(db, input, services = createDocumentServices()) {
1314
1501
  const initiatingSubjectId = cleanString(input.initiatingSubjectId);
1315
1502
  if (!initiatingSubjectId) {
1316
1503
  throw new Error("effective document retrieval requires an initiating subject");
1317
1504
  }
1505
+ const access = await resolveEffectiveDocumentAccess(db, {
1506
+ accountId: input.accountId,
1507
+ workspaceId: input.workspaceId,
1508
+ initiatingSubjectId,
1509
+ surface: input.surface,
1510
+ agentAuthority: input.agentAuthority
1511
+ });
1318
1512
  return await searchDocuments(
1319
1513
  db,
1320
1514
  {
@@ -1328,23 +1522,50 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
1328
1522
  aclTags: input.aclTags,
1329
1523
  // Construct the lower-level access filter here instead of spreading the
1330
1524
  // caller input, so an untyped/legacy access override is always ignored.
1331
- access: {
1332
- viewerSubjectId: initiatingSubjectId,
1333
- ...input.surface === "agent" ? { agentOnly: true } : {}
1334
- }
1525
+ access
1335
1526
  },
1336
1527
  services
1337
1528
  );
1338
1529
  }
1339
1530
  async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
1340
1531
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1341
- const ranked = await searchEffectiveDocuments(
1532
+ const requestedLimit = Math.min(Math.max(input.limit ?? 5, 1), KNOWLEDGE_SEARCH_MAX_RESULTS);
1533
+ const access = await resolveEffectiveDocumentAccess(db, {
1534
+ accountId: input.accountId,
1535
+ workspaceId: input.workspaceId,
1536
+ initiatingSubjectId,
1537
+ surface: input.surface,
1538
+ agentAuthority: input.agentAuthority
1539
+ });
1540
+ const candidateLimit = Math.min(requestedLimit * 4, KNOWLEDGE_SEARCH_MAX_RESULTS);
1541
+ const rankedSelection = await searchDocumentCandidates(
1342
1542
  db,
1343
- { ...input, initiatingSubjectId, surface: "agent" },
1344
- services
1543
+ {
1544
+ accountId: input.accountId,
1545
+ workspaceId: input.workspaceId,
1546
+ query: input.query,
1547
+ limit: candidateLimit,
1548
+ ...input.baseIds ? { baseIds: input.baseIds } : {},
1549
+ ...input.mode ? { mode: input.mode } : {},
1550
+ ...input.sourceKinds ? { sourceKinds: input.sourceKinds } : {},
1551
+ ...input.aclTags ? { aclTags: input.aclTags } : {},
1552
+ access
1553
+ },
1554
+ services,
1555
+ {
1556
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1557
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1558
+ }
1345
1559
  );
1346
- if (ranked.length === 0) return { results: [] };
1347
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1560
+ const ranked = rankedSelection.results;
1561
+ if (ranked.length === 0) {
1562
+ return selectKnowledgeSearchResults({
1563
+ candidates: [],
1564
+ rankedCandidateCount: 0,
1565
+ requestedLimit,
1566
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor
1567
+ });
1568
+ }
1348
1569
  const current = await withDocumentAccountRls(
1349
1570
  db,
1350
1571
  input.accountId,
@@ -1353,7 +1574,9 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
1353
1574
  async (scopedDb) => await scopedDb.select({
1354
1575
  chunk: schema.documentChunks,
1355
1576
  document: schema.documents,
1356
- citation: googleDriveCitationProjection(input.workspaceId, access)
1577
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1578
+ previousChunkId: knowledgePreviousChunkIdProjection(),
1579
+ nextChunkId: knowledgeNextChunkIdProjection()
1357
1580
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1358
1581
  and(
1359
1582
  eq(schema.documents.accountId, input.accountId),
@@ -1368,28 +1591,310 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
1368
1591
  )
1369
1592
  );
1370
1593
  const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
1371
- return {
1372
- results: ranked.flatMap((rankedResult) => {
1594
+ return selectKnowledgeSearchResults({
1595
+ rankedCandidateCount: ranked.length,
1596
+ requestedLimit,
1597
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor,
1598
+ candidates: ranked.flatMap((rankedResult) => {
1373
1599
  const row = currentByChunkId.get(rankedResult.chunkId);
1374
1600
  if (!row) return [];
1375
1601
  return [
1376
1602
  {
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
- }
1603
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
1604
+ previousChunkId: row.previousChunkId,
1605
+ nextChunkId: row.nextChunkId
1606
+ }),
1607
+ semanticScore: rankedResult.score,
1608
+ matchType: rankedResult.matchType,
1609
+ vectorScore: rankedResult.vectorScore,
1610
+ keywordScore: rankedResult.keywordScore
1384
1611
  }
1385
1612
  ];
1386
1613
  })
1614
+ });
1615
+ }
1616
+ function selectKnowledgeSearchResults(input) {
1617
+ const requestedLimit = Math.min(
1618
+ Math.max(Math.trunc(input.requestedLimit), 1),
1619
+ KNOWLEDGE_SEARCH_MAX_RESULTS
1620
+ );
1621
+ const nowMs = (input.now ?? /* @__PURE__ */ new Date()).getTime();
1622
+ let belowRelevanceFloor = Math.min(
1623
+ Math.max(0, Math.trunc(input.alreadyBelowRelevanceFloor ?? 0)),
1624
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS
1625
+ );
1626
+ const relevant = [];
1627
+ for (const candidate of input.candidates) {
1628
+ const relevanceSignals = [];
1629
+ if (candidate.vectorScore !== null && candidate.vectorScore >= KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE) {
1630
+ relevanceSignals.push("vector");
1631
+ }
1632
+ if (candidate.keywordScore !== null && candidate.keywordScore >= KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE) {
1633
+ relevanceSignals.push("keyword");
1634
+ }
1635
+ if (relevanceSignals.length === 0) {
1636
+ belowRelevanceFloor = Math.min(belowRelevanceFloor + 1, KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS);
1637
+ continue;
1638
+ }
1639
+ const freshness = knowledgeFreshness(candidate.record.quality.freshnessAt, nowMs);
1640
+ const qualityAdjustment = freshness === "current" ? 0.02 : freshness === "aging" ? 0.01 : 0;
1641
+ relevant.push({
1642
+ record: candidate.record,
1643
+ retrieval: {
1644
+ score: roundScore(Math.min(1, candidate.semanticScore + qualityAdjustment)),
1645
+ semanticScore: roundScore(candidate.semanticScore),
1646
+ matchType: candidate.matchType,
1647
+ vectorScore: candidate.vectorScore === null ? null : roundScore(candidate.vectorScore),
1648
+ keywordScore: candidate.keywordScore === null ? null : roundScore(candidate.keywordScore),
1649
+ relevanceSignals,
1650
+ freshness,
1651
+ qualityAdjustment,
1652
+ duplicateCount: 0
1653
+ }
1654
+ });
1655
+ }
1656
+ relevant.sort(compareKnowledgeSearchResults);
1657
+ const deduped = [];
1658
+ const byContent = /* @__PURE__ */ new Map();
1659
+ let asDuplicate = 0;
1660
+ for (const result of relevant) {
1661
+ const key = knowledgeTextualContentKey(result.record);
1662
+ const retainedIndex = byContent.get(key);
1663
+ if (retainedIndex === void 0) {
1664
+ byContent.set(key, deduped.length);
1665
+ deduped.push(result);
1666
+ continue;
1667
+ }
1668
+ asDuplicate += 1;
1669
+ const retained = deduped[retainedIndex];
1670
+ retained.retrieval.duplicateCount += 1;
1671
+ }
1672
+ const forLimit = Math.max(0, deduped.length - requestedLimit);
1673
+ const bounded = deduped.slice(0, requestedLimit);
1674
+ let forResponseBudget = 0;
1675
+ let response = knowledgeSearchResponse({
1676
+ results: bounded,
1677
+ rankedCandidateCount: input.rankedCandidateCount,
1678
+ recheckedCandidateCount: input.candidates.length,
1679
+ belowRelevanceFloor,
1680
+ asDuplicate,
1681
+ forLimit,
1682
+ forResponseBudget
1683
+ });
1684
+ while (knowledgeResponseBytes(response) > KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES) {
1685
+ if (bounded.length === 0) {
1686
+ throw new Error("knowledge search selection facts exceed the response budget");
1687
+ }
1688
+ bounded.pop();
1689
+ forResponseBudget += 1;
1690
+ response = knowledgeSearchResponse({
1691
+ results: bounded,
1692
+ rankedCandidateCount: input.rankedCandidateCount,
1693
+ recheckedCandidateCount: input.candidates.length,
1694
+ belowRelevanceFloor,
1695
+ asDuplicate,
1696
+ forLimit,
1697
+ forResponseBudget
1698
+ });
1699
+ }
1700
+ return response;
1701
+ }
1702
+ function compareKnowledgeSearchResults(left, right) {
1703
+ 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);
1704
+ }
1705
+ function knowledgeFreshness(value, nowMs) {
1706
+ const freshnessMs = Date.parse(value);
1707
+ if (!Number.isFinite(freshnessMs)) return "stale";
1708
+ const ageDays = Math.max(0, (nowMs - freshnessMs) / 864e5);
1709
+ if (ageDays <= 90) return "current";
1710
+ return ageDays <= 365 ? "aging" : "stale";
1711
+ }
1712
+ function knowledgeTextualContentKey(record) {
1713
+ return createHash("sha256").update("opengeni:knowledge-search-content:v1\0").update(record.title).update("\0").update(
1714
+ JSON.stringify({
1715
+ body: record.content.body,
1716
+ summary: record.content.summary,
1717
+ topics: record.content.topics
1718
+ })
1719
+ ).digest("hex");
1720
+ }
1721
+ function knowledgeSearchResponse(input) {
1722
+ const selection = {
1723
+ relevanceFloor: {
1724
+ policy: "any_signal",
1725
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1726
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1727
+ },
1728
+ dedupe: { policy: "exact_textual_content" },
1729
+ candidates: {
1730
+ ranked: input.rankedCandidateCount,
1731
+ rechecked: input.recheckedCandidateCount,
1732
+ omittedOnRecheck: Math.max(0, input.rankedCandidateCount - input.recheckedCandidateCount)
1733
+ },
1734
+ omitted: {
1735
+ belowRelevanceFloor: input.belowRelevanceFloor,
1736
+ asDuplicate: input.asDuplicate,
1737
+ forLimit: input.forLimit,
1738
+ forResponseBudget: input.forResponseBudget
1739
+ },
1740
+ budget: {
1741
+ maxResults: KNOWLEDGE_SEARCH_MAX_RESULTS,
1742
+ maxResponseBytes: KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
1743
+ responseBytes: 0,
1744
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
1745
+ estimatedTokens: 0,
1746
+ maxEstimatedTokens: Math.ceil(
1747
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1748
+ )
1749
+ }
1750
+ };
1751
+ const response = {
1752
+ results: [...input.results],
1753
+ selection
1754
+ };
1755
+ for (let index = 0; index < 8; index += 1) {
1756
+ const responseBytes = knowledgeResponseBytes(response);
1757
+ const estimatedTokens = Math.ceil(
1758
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1759
+ );
1760
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
1761
+ break;
1762
+ }
1763
+ response.selection.budget.responseBytes = responseBytes;
1764
+ response.selection.budget.estimatedTokens = estimatedTokens;
1765
+ }
1766
+ return response;
1767
+ }
1768
+ function knowledgeResponseBytes(response) {
1769
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1770
+ }
1771
+ function selectKnowledgeBrowseRecords(input) {
1772
+ const entries = input.entries.slice(0, KNOWLEDGE_BROWSE_MAX_LIMIT);
1773
+ const selected = entries.map((entry) => entry.record);
1774
+ let omittedForResponseBudget = 0;
1775
+ let compactedRecordCount = 0;
1776
+ let response = knowledgeBrowseResponse({
1777
+ records: selected,
1778
+ nextCursor: input.hasMoreAfterEntries ? entries.at(-1)?.cursorAfter ?? null : null,
1779
+ hasMore: input.hasMoreAfterEntries,
1780
+ omittedForResponseBudget,
1781
+ compactedRecordCount
1782
+ });
1783
+ while (selected.length > 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1784
+ selected.pop();
1785
+ omittedForResponseBudget += 1;
1786
+ response = knowledgeBrowseResponse({
1787
+ records: selected,
1788
+ nextCursor: entries[selected.length - 1]?.cursorAfter ?? null,
1789
+ hasMore: true,
1790
+ omittedForResponseBudget,
1791
+ compactedRecordCount
1792
+ });
1793
+ }
1794
+ if (selected.length === 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1795
+ selected[0] = compactKnowledgeBrowseRecord(selected[0]);
1796
+ compactedRecordCount = 1;
1797
+ response = knowledgeBrowseResponse({
1798
+ records: selected,
1799
+ nextCursor: omittedForResponseBudget > 0 || input.hasMoreAfterEntries ? entries[0]?.cursorAfter ?? null : null,
1800
+ hasMore: omittedForResponseBudget > 0 || input.hasMoreAfterEntries,
1801
+ omittedForResponseBudget,
1802
+ compactedRecordCount
1803
+ });
1804
+ }
1805
+ if (knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1806
+ throw new Error("knowledge browse discovery projection exceeds the response budget");
1807
+ }
1808
+ return response;
1809
+ }
1810
+ function compactKnowledgeBrowseRecord(record) {
1811
+ const fields = /* @__PURE__ */ new Set([
1812
+ ...record.projection.fields,
1813
+ "content.body",
1814
+ "content.summary",
1815
+ "content.topics",
1816
+ "content.metadata",
1817
+ "provenance.source.uri",
1818
+ "provenance.source.externalId",
1819
+ "provenance.source.title",
1820
+ "provenance.source.author",
1821
+ "provenance.source.version",
1822
+ "provenance.citation"
1823
+ ]);
1824
+ return {
1825
+ ...record,
1826
+ content: {
1827
+ format: "markdown",
1828
+ body: null,
1829
+ summary: null,
1830
+ topics: [],
1831
+ metadata: {}
1832
+ },
1833
+ provenance: {
1834
+ ...record.provenance,
1835
+ source: {
1836
+ ...record.provenance.source,
1837
+ uri: null,
1838
+ externalId: null,
1839
+ title: null,
1840
+ author: null,
1841
+ version: null
1842
+ },
1843
+ citation: null
1844
+ },
1845
+ links: record.links.filter((link) => link.target.kind === "knowledge"),
1846
+ projection: {
1847
+ truncated: true,
1848
+ fields: [...fields].sort()
1849
+ }
1850
+ };
1851
+ }
1852
+ function knowledgeBrowseResponse(input) {
1853
+ const response = {
1854
+ records: [...input.records],
1855
+ nextCursor: input.nextCursor,
1856
+ hasMore: input.hasMore,
1857
+ selection: {
1858
+ omitted: { forResponseBudget: input.omittedForResponseBudget },
1859
+ compactedRecordCount: input.compactedRecordCount,
1860
+ budget: {
1861
+ maxResults: KNOWLEDGE_BROWSE_MAX_LIMIT,
1862
+ maxResponseBytes: KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
1863
+ responseBytes: 0,
1864
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
1865
+ estimatedTokens: 0,
1866
+ maxEstimatedTokens: Math.ceil(
1867
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1868
+ )
1869
+ }
1870
+ }
1387
1871
  };
1872
+ for (let index = 0; index < 8; index += 1) {
1873
+ const responseBytes = knowledgeBrowseResponseBytes(response);
1874
+ const estimatedTokens = Math.ceil(
1875
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1876
+ );
1877
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
1878
+ break;
1879
+ }
1880
+ response.selection.budget.responseBytes = responseBytes;
1881
+ response.selection.budget.estimatedTokens = estimatedTokens;
1882
+ }
1883
+ return response;
1884
+ }
1885
+ function knowledgeBrowseResponseBytes(response) {
1886
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1388
1887
  }
1389
1888
  async function getEffectiveKnowledgeRecord(db, input) {
1390
1889
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1391
1890
  const target = parseKnowledgeRecordId(input.id);
1392
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1891
+ const access = await resolveEffectiveDocumentAccess(db, {
1892
+ accountId: input.accountId,
1893
+ workspaceId: input.workspaceId,
1894
+ initiatingSubjectId,
1895
+ surface: input.surface ?? "agent",
1896
+ agentAuthority: input.agentAuthority
1897
+ });
1393
1898
  return await withDocumentAccountRls(
1394
1899
  db,
1395
1900
  input.accountId,
@@ -1399,8 +1904,16 @@ async function getEffectiveKnowledgeRecord(db, input) {
1399
1904
  if (target.kind === "document") {
1400
1905
  const [row2] = await scopedDb.select({
1401
1906
  document: schema.documents,
1402
- citation: googleDriveCitationProjection(input.workspaceId, access)
1403
- }).from(schema.documents).where(
1907
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1908
+ firstChunkId: schema.documentChunks.id
1909
+ }).from(schema.documents).leftJoin(
1910
+ schema.documentChunks,
1911
+ and(
1912
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
1913
+ eq(schema.documentChunks.documentId, schema.documents.id),
1914
+ eq(schema.documentChunks.chunkIndex, 0)
1915
+ )
1916
+ ).where(
1404
1917
  and(
1405
1918
  eq(schema.documents.accountId, input.accountId),
1406
1919
  eq(schema.documents.id, target.id),
@@ -1408,12 +1921,14 @@ async function getEffectiveKnowledgeRecord(db, input) {
1408
1921
  ...documentAccessConditions(input.workspaceId, access)
1409
1922
  )
1410
1923
  ).limit(1);
1411
- return row2 ? knowledgeDocumentRecord(row2.document, row2.citation) : null;
1924
+ return row2 ? knowledgeDocumentRecord(row2.document, row2.citation, row2.firstChunkId) : null;
1412
1925
  }
1413
1926
  const [row] = await scopedDb.select({
1414
1927
  chunk: schema.documentChunks,
1415
1928
  document: schema.documents,
1416
- citation: googleDriveCitationProjection(input.workspaceId, access)
1929
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1930
+ previousChunkId: knowledgePreviousChunkIdProjection(),
1931
+ nextChunkId: knowledgeNextChunkIdProjection()
1417
1932
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1418
1933
  and(
1419
1934
  eq(schema.documents.accountId, input.accountId),
@@ -1423,7 +1938,10 @@ async function getEffectiveKnowledgeRecord(db, input) {
1423
1938
  ...documentAccessConditions(input.workspaceId, access)
1424
1939
  )
1425
1940
  ).limit(1);
1426
- return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation) : null;
1941
+ return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation, {
1942
+ previousChunkId: row.previousChunkId,
1943
+ nextChunkId: row.nextChunkId
1944
+ }) : null;
1427
1945
  }
1428
1946
  );
1429
1947
  }
@@ -1453,11 +1971,14 @@ async function browseEffectiveKnowledge(db, input) {
1453
1971
  topic,
1454
1972
  sourceKinds
1455
1973
  };
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 };
1974
+ const topLevelAfter = !parent && input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
1975
+ const access = await resolveEffectiveDocumentAccess(db, {
1976
+ accountId: input.accountId,
1977
+ workspaceId: input.workspaceId,
1978
+ initiatingSubjectId,
1979
+ surface: input.surface ?? "agent",
1980
+ agentAuthority: input.agentAuthority
1981
+ });
1461
1982
  return await withDocumentAccountRls(
1462
1983
  db,
1463
1984
  input.accountId,
@@ -1465,7 +1986,7 @@ async function browseEffectiveKnowledge(db, input) {
1465
1986
  access,
1466
1987
  async (scopedDb) => {
1467
1988
  if (parent) {
1468
- const [authorizedParent] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
1989
+ const [authorizedParent] = await scopedDb.select({ id: schema.documents.id, indexSequence: schema.documents.indexSequence }).from(schema.documents).where(
1469
1990
  and(
1470
1991
  eq(schema.documents.accountId, input.accountId),
1471
1992
  eq(schema.documents.id, parent.id),
@@ -1473,34 +1994,57 @@ async function browseEffectiveKnowledge(db, input) {
1473
1994
  ...documentAccessConditions(input.workspaceId, access)
1474
1995
  )
1475
1996
  ).limit(1);
1476
- if (!authorizedParent) return { records: [], nextCursor: null, hasMore: false };
1997
+ if (!authorizedParent) {
1998
+ return selectKnowledgeBrowseRecords({ entries: [], hasMoreAfterEntries: false });
1999
+ }
2000
+ if (authorizedParent.indexSequence === null) {
2001
+ throw new Error("ready knowledge document is missing its index revision");
2002
+ }
2003
+ const parentCursorScope = {
2004
+ ...cursorScope,
2005
+ parentRevision: authorizedParent.indexSequence.toString()
2006
+ };
2007
+ const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, parentCursorScope) : 0n;
2008
+ if (after > 2147483648n) {
2009
+ throw new Error("invalid knowledge browse cursor");
2010
+ }
1477
2011
  const rows2 = await scopedDb.select({
1478
2012
  chunk: schema.documentChunks,
1479
2013
  document: schema.documents,
1480
- citation: googleDriveCitationProjection(input.workspaceId, access)
2014
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2015
+ previousChunkId: knowledgePreviousChunkIdProjection(),
2016
+ nextChunkId: knowledgeNextChunkIdProjection()
1481
2017
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1482
2018
  and(
1483
2019
  eq(schema.documentChunks.accountId, input.accountId),
1484
2020
  eq(schema.documentChunks.documentId, parent.id),
1485
2021
  gt(schema.documentChunks.chunkIndex, Number(after) - 1),
1486
2022
  eq(schema.documents.status, "ready"),
2023
+ eq(schema.documents.indexSequence, authorizedParent.indexSequence),
1487
2024
  ...documentAccessConditions(input.workspaceId, access)
1488
2025
  )
1489
2026
  ).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
1490
2027
  const hasMore2 = rows2.length > limit;
1491
2028
  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
- };
2029
+ return selectKnowledgeBrowseRecords({
2030
+ entries: page2.map((row) => ({
2031
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
2032
+ previousChunkId: row.previousChunkId,
2033
+ nextChunkId: row.nextChunkId
2034
+ }),
2035
+ cursorAfter: encodeKnowledgeBrowseCursor(
2036
+ parentCursorScope,
2037
+ BigInt(row.chunk.chunkIndex + 1)
2038
+ )
2039
+ })),
2040
+ hasMoreAfterEntries: hasMore2
2041
+ });
1498
2042
  }
1499
2043
  const conditions = [
1500
2044
  eq(schema.documents.accountId, input.accountId),
1501
2045
  eq(schema.documents.status, "ready"),
1502
2046
  isNotNull(schema.documents.indexSequence),
1503
- gt(schema.documents.indexSequence, after),
2047
+ gt(schema.documents.indexSequence, topLevelAfter),
1504
2048
  ...documentAccessConditions(input.workspaceId, access)
1505
2049
  ];
1506
2050
  if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
@@ -1508,23 +2052,38 @@ async function browseEffectiveKnowledge(db, input) {
1508
2052
  conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
1509
2053
  const rows = await scopedDb.select({
1510
2054
  document: schema.documents,
1511
- citation: googleDriveCitationProjection(input.workspaceId, access)
1512
- }).from(schema.documents).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
2055
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2056
+ firstChunkId: schema.documentChunks.id
2057
+ }).from(schema.documents).leftJoin(
2058
+ schema.documentChunks,
2059
+ and(
2060
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
2061
+ eq(schema.documentChunks.documentId, schema.documents.id),
2062
+ eq(schema.documentChunks.chunkIndex, 0)
2063
+ )
2064
+ ).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
1513
2065
  const hasMore = rows.length > limit;
1514
2066
  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
- };
2067
+ return selectKnowledgeBrowseRecords({
2068
+ entries: page.map((row) => {
2069
+ if (row.document.indexSequence === null) {
2070
+ throw new Error("ready knowledge document is missing its index revision");
2071
+ }
2072
+ return {
2073
+ record: knowledgeDocumentRecord(row.document, row.citation, row.firstChunkId),
2074
+ cursorAfter: encodeKnowledgeBrowseCursor(cursorScope, row.document.indexSequence)
2075
+ };
2076
+ }),
2077
+ hasMoreAfterEntries: hasMore
2078
+ });
1521
2079
  }
1522
2080
  );
1523
2081
  }
1524
2082
  function encodeKnowledgeBrowseCursor(scope, position) {
1525
2083
  if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
2084
+ const version = knowledgeBrowseCursorVersion(scope);
1526
2085
  return Buffer.from(
1527
- JSON.stringify({ v: 1, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
2086
+ JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
1528
2087
  "utf8"
1529
2088
  ).toString("base64url");
1530
2089
  }
@@ -1536,7 +2095,8 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1536
2095
  const bytes = Buffer.from(value, "base64url");
1537
2096
  if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1538
2097
  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)) {
2098
+ const expectedVersion = knowledgeBrowseCursorVersion(scope);
2099
+ 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
2100
  throw new Error("cursor payload");
1541
2101
  }
1542
2102
  if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
@@ -1553,7 +2113,17 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1553
2113
  }
1554
2114
  }
1555
2115
  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");
2116
+ const version = knowledgeBrowseCursorVersion(scope);
2117
+ 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");
2118
+ if (version === 2) hash.update(scope.parentRevision).update("\0");
2119
+ return hash.update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
2120
+ }
2121
+ function knowledgeBrowseCursorVersion(scope) {
2122
+ if (!scope.parentId) return 1;
2123
+ if (!scope.parentRevision || !/^[1-9][0-9]*$/.test(scope.parentRevision)) {
2124
+ throw new Error("knowledge browse parent cursor requires an exact document revision");
2125
+ }
2126
+ return 2;
1557
2127
  }
1558
2128
  function parseKnowledgeRecordId(value) {
1559
2129
  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 +2132,7 @@ function parseKnowledgeRecordId(value) {
1562
2132
  if (!match) throw new Error("invalid knowledge record id");
1563
2133
  return { kind: match[1], id: match[2].toLowerCase() };
1564
2134
  }
1565
- function knowledgeDocumentRecord(document, citation = null) {
2135
+ function knowledgeDocumentRecord(document, citation = null, firstChunkId = null) {
1566
2136
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1567
2137
  const projected = projectKnowledgeRecord({
1568
2138
  title: document.title,
@@ -1585,11 +2155,22 @@ function knowledgeDocumentRecord(document, citation = null) {
1585
2155
  },
1586
2156
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1587
2157
  quality: knowledgeQuality(document),
1588
- links: knowledgeSourceLinks(projected.source.uri),
2158
+ links: [
2159
+ ...firstChunkId ? [
2160
+ {
2161
+ relation: "contents",
2162
+ target: {
2163
+ kind: "knowledge",
2164
+ id: `document_chunk:${firstChunkId}`
2165
+ }
2166
+ }
2167
+ ] : [],
2168
+ ...knowledgeSourceLinks(projected.source.uri)
2169
+ ],
1589
2170
  projection: projected.projection
1590
2171
  };
1591
2172
  }
1592
- function knowledgeChunkRecord(document, chunk, citation = null) {
2173
+ function knowledgeChunkRecord(document, chunk, citation = null, traversal = { previousChunkId: null, nextChunkId: null }) {
1593
2174
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1594
2175
  const projected = projectKnowledgeRecord({
1595
2176
  title: document.title,
@@ -1613,7 +2194,28 @@ function knowledgeChunkRecord(document, chunk, citation = null) {
1613
2194
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1614
2195
  quality: knowledgeQuality(document),
1615
2196
  links: [
1616
- { relation: "parent", target: { kind: "knowledge", id: `document:${document.id}` } },
2197
+ {
2198
+ relation: "parent",
2199
+ target: { kind: "knowledge", id: `document:${document.id}` }
2200
+ },
2201
+ ...traversal.previousChunkId ? [
2202
+ {
2203
+ relation: "previous",
2204
+ target: {
2205
+ kind: "knowledge",
2206
+ id: `document_chunk:${traversal.previousChunkId}`
2207
+ }
2208
+ }
2209
+ ] : [],
2210
+ ...traversal.nextChunkId ? [
2211
+ {
2212
+ relation: "next",
2213
+ target: {
2214
+ kind: "knowledge",
2215
+ id: `document_chunk:${traversal.nextChunkId}`
2216
+ }
2217
+ }
2218
+ ] : [],
1617
2219
  ...knowledgeSourceLinks(projected.source.uri)
1618
2220
  ],
1619
2221
  projection: projected.projection
@@ -1643,6 +2245,28 @@ function knowledgeQuality(document) {
1643
2245
  function knowledgeSourceLinks(sourceUri) {
1644
2246
  return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
1645
2247
  }
2248
+ function knowledgePreviousChunkIdProjection() {
2249
+ return sql`(
2250
+ select knowledge_previous_chunk.id
2251
+ from document_chunks knowledge_previous_chunk
2252
+ where knowledge_previous_chunk.account_id = ${schema.documentChunks.accountId}
2253
+ and knowledge_previous_chunk.document_id = ${schema.documentChunks.documentId}
2254
+ and knowledge_previous_chunk.chunk_index < ${schema.documentChunks.chunkIndex}
2255
+ order by knowledge_previous_chunk.chunk_index desc
2256
+ limit 1
2257
+ )`;
2258
+ }
2259
+ function knowledgeNextChunkIdProjection() {
2260
+ return sql`(
2261
+ select knowledge_next_chunk.id
2262
+ from document_chunks knowledge_next_chunk
2263
+ where knowledge_next_chunk.account_id = ${schema.documentChunks.accountId}
2264
+ and knowledge_next_chunk.document_id = ${schema.documentChunks.documentId}
2265
+ and knowledge_next_chunk.chunk_index > ${schema.documentChunks.chunkIndex}
2266
+ order by knowledge_next_chunk.chunk_index asc
2267
+ limit 1
2268
+ )`;
2269
+ }
1646
2270
  async function vectorSearchDocuments(db, input, limit, services) {
1647
2271
  const queryEmbedding = await services.embedder.embedQuery(input.query);
1648
2272
  validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
@@ -1676,7 +2300,7 @@ async function vectorSearchDocuments(db, input, limit, services) {
1676
2300
  authoritySubjectId: schema.documents.authoritySubjectId,
1677
2301
  citation: googleDriveCitationProjection(input.workspaceId, input.access),
1678
2302
  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)
2303
+ }).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
2304
  );
1681
2305
  return rows.map((row) => ({
1682
2306
  ...mapSearchRowBase(row),
@@ -1720,7 +2344,7 @@ async function keywordSearchDocuments(db, input, limit) {
1720
2344
  ...documentSearchConditions(input),
1721
2345
  sql`to_tsvector('simple', ${schema.documentChunks.text}) @@ plainto_tsquery('simple', ${input.query})`
1722
2346
  )
1723
- ).orderBy(desc(rank)).limit(limit)
2347
+ ).orderBy(desc(rank), asc(schema.documentChunks.id)).limit(limit)
1724
2348
  );
1725
2349
  return rows.map((row) => ({
1726
2350
  ...mapSearchRowBase(row),
@@ -1816,6 +2440,21 @@ async function assertDocumentAccountWorkspace(db, accountId, workspaceId) {
1816
2440
  }
1817
2441
  return context;
1818
2442
  }
2443
+ async function resolveEffectiveDocumentAccess(_db, input) {
2444
+ if (input.surface === "human") {
2445
+ return { viewerSubjectId: input.initiatingSubjectId };
2446
+ }
2447
+ return {
2448
+ agentOnly: true,
2449
+ viewerSubjectId: input.initiatingSubjectId,
2450
+ authorizedPersonalAttempt: input.agentAuthority ? {
2451
+ accountId: input.accountId,
2452
+ workspaceId: input.workspaceId,
2453
+ sessionId: input.agentAuthority.sessionId,
2454
+ attemptId: input.agentAuthority.attemptId
2455
+ } : void 0
2456
+ };
2457
+ }
1819
2458
  function documentAccessConditions(workspaceId, access) {
1820
2459
  const organization = eq(schema.documents.authorityKind, "organization");
1821
2460
  const workspace = and(
@@ -1823,10 +2462,28 @@ function documentAccessConditions(workspaceId, access) {
1823
2462
  eq(schema.documents.authorityWorkspaceId, workspaceId)
1824
2463
  );
1825
2464
  const viewer = cleanString(access?.viewerSubjectId ?? null);
2465
+ const personalAttempt = access?.authorizedPersonalAttempt;
2466
+ const authorizedPersonal = personalAttempt ? sql`${schema.documents.id} IN (
2467
+ SELECT resolve_session_attempt_personal_document_reads(
2468
+ ${personalAttempt.accountId}::uuid,
2469
+ ${personalAttempt.workspaceId}::uuid,
2470
+ ${personalAttempt.sessionId}::uuid,
2471
+ ${personalAttempt.attemptId}::uuid
2472
+ )
2473
+ )` : sql`false`;
1826
2474
  const personal = viewer ? and(
1827
2475
  eq(schema.documents.authorityKind, "personal"),
1828
- eq(schema.documents.authorityWorkspaceId, workspaceId),
1829
- eq(schema.documents.authoritySubjectId, viewer)
2476
+ eq(schema.documents.authoritySubjectId, viewer),
2477
+ access?.agentOnly ? or(
2478
+ and(
2479
+ isNull(schema.documents.authorityId),
2480
+ eq(schema.documents.authorityWorkspaceId, workspaceId)
2481
+ ),
2482
+ and(isNotNull(schema.documents.authorityId), authorizedPersonal)
2483
+ ) ?? authorizedPersonal : or(
2484
+ eq(schema.documents.authorityWorkspaceId, workspaceId),
2485
+ isNull(schema.documents.authorityWorkspaceId)
2486
+ ) ?? eq(schema.documents.authorityWorkspaceId, workspaceId)
1830
2487
  ) : void 0;
1831
2488
  const authority = viewer ? or(organization, workspace, personal) ?? organization : or(organization, workspace) ?? organization;
1832
2489
  const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
@@ -1843,7 +2500,7 @@ function documentAccessConditions(workspaceId, access) {
1843
2500
  }
1844
2501
  function documentMatchesAccess(document, workspaceId, access) {
1845
2502
  if (access?.agentOnly) {
1846
- return document.agentAccess && canViewDocument(document, access.viewerSubjectId, workspaceId);
2503
+ return document.agentAccess && (document.authorityKind !== "personal" || document.authorityId === null) && canViewDocument(document, access.viewerSubjectId, workspaceId);
1847
2504
  }
1848
2505
  return canViewDocument(document, access?.viewerSubjectId, workspaceId);
1849
2506
  }
@@ -1861,16 +2518,16 @@ function canViewDocument(document, viewerSubjectId, workspaceId) {
1861
2518
  return document.authorityWorkspaceId === null && document.authoritySubjectId === null;
1862
2519
  }
1863
2520
  const normalizedWorkspaceId = cleanString(workspaceId ?? null);
1864
- if (!normalizedWorkspaceId || document.authorityWorkspaceId !== normalizedWorkspaceId) {
2521
+ if (!normalizedWorkspaceId) {
1865
2522
  return false;
1866
2523
  }
1867
2524
  if (document.authorityKind === "workspace") {
1868
- return document.authoritySubjectId === null;
2525
+ return document.authorityWorkspaceId === normalizedWorkspaceId && document.authoritySubjectId === null;
1869
2526
  }
1870
2527
  if (document.authorityKind === "personal") {
1871
2528
  const authoritySubjectId = canonicalDocumentAuthoritySubject(document.authoritySubjectId);
1872
2529
  const viewer = canonicalDocumentAuthoritySubject(viewerSubjectId);
1873
- return !!authoritySubjectId && authoritySubjectId === viewer;
2530
+ return !!authoritySubjectId && authoritySubjectId === viewer && (document.authorityWorkspaceId === null || document.authorityWorkspaceId === normalizedWorkspaceId);
1874
2531
  }
1875
2532
  return false;
1876
2533
  }
@@ -1960,7 +2617,7 @@ function mergeDocumentSearchRows(rows, mode) {
1960
2617
  matchType
1961
2618
  };
1962
2619
  }).sort(
1963
- (left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex
2620
+ (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
2621
  );
1965
2622
  }
1966
2623
  function combinedSearchScore(mode, vectorScore, keywordScore, matchType) {
@@ -2147,6 +2804,7 @@ function mapDocument(row) {
2147
2804
  authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
2148
2805
  authorityWorkspaceId: row.authorityWorkspaceId,
2149
2806
  authoritySubjectId: row.authoritySubjectId,
2807
+ authorityId: row.authorityId,
2150
2808
  visibility: normalizeDocumentVisibility(row.visibility),
2151
2809
  createdBy: row.createdBy,
2152
2810
  agentAccess: row.agentAccess,
@@ -2253,9 +2911,12 @@ export {
2253
2911
  getDocumentChunk,
2254
2912
  getDocumentForIndexing,
2255
2913
  getDocumentInventory,
2914
+ getDocumentOriginalFile,
2256
2915
  getEffectiveKnowledgeRecord,
2257
2916
  heuristicCuration,
2258
2917
  indexDocumentNow,
2918
+ listAccessibleDocuments,
2919
+ listDocumentAuthorityReclassifications,
2259
2920
  listDocumentBases,
2260
2921
  listDocumentBasesEnsuringDefault,
2261
2922
  listDocuments,
@@ -2265,9 +2926,15 @@ export {
2265
2926
  parseDocumentBytes,
2266
2927
  projectKnowledgeRecord,
2267
2928
  queueDocumentForReindex,
2929
+ reclassifyDocumentAuthority,
2268
2930
  resolveDocumentAuthority,
2931
+ resolveEffectiveDocumentAccess,
2932
+ runDocumentDefaultCollectionBackfill,
2269
2933
  searchDocuments,
2270
2934
  searchEffectiveDocuments,
2271
- searchEffectiveKnowledge
2935
+ searchEffectiveKnowledge,
2936
+ selectDocumentSearchCandidateWindow,
2937
+ selectKnowledgeBrowseRecords,
2938
+ selectKnowledgeSearchResults
2272
2939
  };
2273
2940
  //# sourceMappingURL=index.js.map