@gmickel/gno 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +20 -16
  2. package/assets/skill/SKILL.md +8 -5
  3. package/package.json +1 -1
  4. package/src/app/context-agent-projection.ts +303 -0
  5. package/src/app/context-format.ts +249 -0
  6. package/src/app/context-runtime-contract.ts +325 -0
  7. package/src/app/context-runtime-input.ts +362 -0
  8. package/src/app/context-runtime-types.ts +65 -0
  9. package/src/app/context-runtime.ts +170 -0
  10. package/src/app/context-surface.ts +145 -0
  11. package/src/cli/commands/context-build.ts +149 -0
  12. package/src/cli/commands/context-verify.ts +90 -0
  13. package/src/cli/options.ts +4 -0
  14. package/src/cli/program.ts +178 -0
  15. package/src/core/context-budget.ts +461 -0
  16. package/src/core/context-capsule-index-schema.ts +15 -0
  17. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  18. package/src/core/context-capsule-schema.ts +473 -0
  19. package/src/core/context-capsule-validation.ts +416 -0
  20. package/src/core/context-capsule-verification.ts +218 -0
  21. package/src/core/context-capsule.ts +439 -0
  22. package/src/core/context-compiler.ts +513 -0
  23. package/src/core/context-evidence-metadata.ts +33 -0
  24. package/src/core/context-evidence.ts +495 -0
  25. package/src/core/context-facets.ts +163 -0
  26. package/src/core/context-guidance.ts +69 -0
  27. package/src/core/context-scope.ts +32 -0
  28. package/src/core/context-verifier-canonical.ts +90 -0
  29. package/src/core/context-verifier-input.ts +66 -0
  30. package/src/core/context-verifier.ts +447 -0
  31. package/src/core/sections.ts +63 -0
  32. package/src/mcp/server.ts +10 -4
  33. package/src/mcp/tools/context.ts +229 -0
  34. package/src/mcp/tools/index.ts +27 -0
  35. package/src/pipeline/chunk-lookup.ts +33 -0
  36. package/src/pipeline/hybrid.ts +79 -57
  37. package/src/pipeline/types.ts +14 -0
  38. package/src/sdk/client.ts +68 -6
  39. package/src/sdk/index.ts +21 -0
  40. package/src/sdk/types.ts +24 -0
  41. package/src/serve/background-runtime.ts +1 -0
  42. package/src/serve/context-capsule.ts +136 -0
  43. package/src/serve/context.ts +10 -1
  44. package/src/serve/routes/api.ts +2 -0
  45. package/src/serve/server.ts +23 -0
  46. package/src/store/sqlite/adapter.ts +38 -20
package/src/sdk/client.ts CHANGED
@@ -22,6 +22,9 @@ import type {
22
22
  GnoCreateFolderResult,
23
23
  GnoCreateNoteOptions,
24
24
  GnoCreateNoteResult,
25
+ GnoContextInput,
26
+ GnoContextResult,
27
+ GnoContextVerificationResult,
25
28
  GnoClientInitOptions,
26
29
  GnoDuplicateNoteOptions,
27
30
  GnoEmbedOptions,
@@ -39,8 +42,21 @@ import type {
39
42
  GnoVectorSearchOptions,
40
43
  } from "./types";
41
44
 
42
- import { decorateUriForIndex, getIndexDbPath } from "../app/constants";
43
- import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
45
+ import {
46
+ decorateUriForIndex,
47
+ DEFAULT_INDEX_NAME,
48
+ getIndexDbPath,
49
+ } from "../app/constants";
50
+ import {
51
+ buildContextCapsule,
52
+ validateContextCapsuleBuildInput,
53
+ verifyContextCapsuleRuntime,
54
+ } from "../app/context-runtime";
55
+ import {
56
+ canonicalizeIndexName,
57
+ INDEX_NAME_REQUIREMENTS,
58
+ isValidIndexName,
59
+ } from "../app/index-name";
44
60
  import {
45
61
  ConfigSchema,
46
62
  loadConfig,
@@ -108,7 +124,7 @@ interface OpenedClientState {
108
124
  store: SqliteAdapter;
109
125
  llm: LlmAdapter;
110
126
  downloadPolicy: DownloadPolicy;
111
- indexName?: string;
127
+ indexName: string;
112
128
  }
113
129
 
114
130
  interface RuntimePorts {
@@ -161,7 +177,10 @@ async function resolveClientState(
161
177
  configSource = "file";
162
178
  }
163
179
 
164
- const dbPath = options.dbPath ?? getIndexDbPath(options.indexName);
180
+ const indexName = canonicalizeIndexName(
181
+ options.indexName ?? DEFAULT_INDEX_NAME
182
+ );
183
+ const dbPath = options.dbPath ?? getIndexDbPath(indexName);
165
184
  await mkdir(dirname(dbPath), { recursive: true });
166
185
 
167
186
  const store = new SqliteAdapter();
@@ -179,7 +198,7 @@ async function resolveClientState(
179
198
  llm: new LlmAdapter(config, options.cacheDir),
180
199
  downloadPolicy:
181
200
  options.downloadPolicy ?? resolveDownloadPolicy(process.env, {}),
182
- indexName: options.indexName,
201
+ indexName,
183
202
  };
184
203
  }
185
204
 
@@ -192,7 +211,7 @@ class GnoClientImpl implements GnoClient {
192
211
  private readonly store: SqliteAdapter;
193
212
  private readonly llm: LlmAdapter;
194
213
  private readonly downloadPolicy: DownloadPolicy;
195
- private readonly indexName?: string;
214
+ private readonly indexName: string;
196
215
  private closed = false;
197
216
 
198
217
  constructor(state: OpenedClientState) {
@@ -644,6 +663,49 @@ class GnoClientImpl implements GnoClient {
644
663
  }
645
664
  }
646
665
 
666
+ async context(input: GnoContextInput): Promise<GnoContextResult> {
667
+ this.assertOpen();
668
+ validateContextCapsuleBuildInput(
669
+ { ...input, indexName: this.indexName },
670
+ this.indexName,
671
+ this.config.collections.map((collection) => collection.name)
672
+ );
673
+ const collection =
674
+ input.collections?.length === 1 ? input.collections[0] : undefined;
675
+ const useModels = input.depthPolicy !== "fast";
676
+ const ports = await this.createRuntimePorts({
677
+ embed: useModels,
678
+ rerank: useModels,
679
+ collection,
680
+ });
681
+ try {
682
+ return await buildContextCapsule(
683
+ { ...input, indexName: this.indexName },
684
+ {
685
+ store: this.store,
686
+ config: this.config,
687
+ indexName: this.indexName,
688
+ vectorIndex: ports.vectorIndex,
689
+ embedPort: ports.embedPort,
690
+ rerankPort: ports.rerankPort,
691
+ }
692
+ );
693
+ } finally {
694
+ await this.disposeRuntimePorts(ports);
695
+ }
696
+ }
697
+
698
+ async verifyContext(
699
+ capsule: GnoContextResult
700
+ ): Promise<GnoContextVerificationResult> {
701
+ this.assertOpen();
702
+ return verifyContextCapsuleRuntime(capsule, {
703
+ store: this.store,
704
+ config: this.config,
705
+ indexName: this.indexName,
706
+ });
707
+ }
708
+
647
709
  async get(ref: string, options: GnoGetOptions = {}) {
648
710
  this.assertOpen();
649
711
  const resolution = resolveEffectiveIndex([ref], this.indexName);
package/src/sdk/index.ts CHANGED
@@ -31,6 +31,10 @@ export type {
31
31
  GnoAskOptions,
32
32
  GnoCaptureOptions,
33
33
  GnoCaptureResult,
34
+ GnoContextErrorCode,
35
+ GnoContextInput,
36
+ GnoContextResult,
37
+ GnoContextVerificationResult,
34
38
  GnoClient,
35
39
  GnoClientInitOptions,
36
40
  GnoEmbedOptions,
@@ -51,3 +55,20 @@ export type {
51
55
  GnoUpdateOptions,
52
56
  GnoVectorSearchOptions,
53
57
  } from "./types";
58
+ export {
59
+ ContextCapsuleContractError,
60
+ type ContextCapsuleErrorCode,
61
+ type ContextCapsuleV1,
62
+ } from "../core/context-capsule";
63
+ export {
64
+ ContextEvidenceError,
65
+ type ContextEvidenceErrorCode,
66
+ } from "../core/context-evidence";
67
+ export {
68
+ ContextVerifierError,
69
+ type ContextVerifierErrorCode,
70
+ } from "../core/context-verifier";
71
+ export {
72
+ ContextRuntimeError,
73
+ type ContextRuntimeErrorCode,
74
+ } from "../app/context-runtime";
package/src/sdk/types.ts CHANGED
@@ -4,8 +4,19 @@
4
4
  * @module src/sdk/types
5
5
  */
6
6
 
7
+ import type {
8
+ ContextCapsuleBuildInput,
9
+ ContextRuntimeErrorCode,
10
+ } from "../app/context-runtime";
7
11
  import type { Config } from "../config/types";
8
12
  import type { CaptureInput, CaptureReceipt } from "../core/capture";
13
+ import type {
14
+ ContextCapsuleErrorCode,
15
+ ContextCapsuleV1,
16
+ ContextCapsuleVerification,
17
+ } from "../core/context-capsule";
18
+ import type { ContextEvidenceErrorCode } from "../core/context-evidence";
19
+ import type { ContextVerifierErrorCode } from "../core/context-verifier";
9
20
  import type { NoteCollisionPolicy } from "../core/note-creation";
10
21
  import type { NotePresetId } from "../core/note-presets";
11
22
  import type { DocumentSection } from "../core/sections";
@@ -65,6 +76,15 @@ export type GnoVectorSearchOptions = SearchOptions & {
65
76
  model?: string;
66
77
  };
67
78
 
79
+ export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName">;
80
+ export type GnoContextResult = ContextCapsuleV1;
81
+ export type GnoContextVerificationResult = ContextCapsuleVerification;
82
+ export type GnoContextErrorCode =
83
+ | ContextRuntimeErrorCode
84
+ | ContextCapsuleErrorCode
85
+ | ContextEvidenceErrorCode
86
+ | ContextVerifierErrorCode;
87
+
68
88
  export interface GnoGetOptions {
69
89
  from?: number;
70
90
  limit?: number;
@@ -185,6 +205,10 @@ export interface GnoClient {
185
205
  ): Promise<SearchResults>;
186
206
  query(query: string, options?: GnoQueryOptions): Promise<SearchResults>;
187
207
  ask(query: string, options?: GnoAskOptions): Promise<AskResult>;
208
+ context(input: GnoContextInput): Promise<GnoContextResult>;
209
+ verifyContext(
210
+ capsule: ContextCapsuleV1
211
+ ): Promise<GnoContextVerificationResult>;
188
212
  get(
189
213
  ref: string,
190
214
  options?: GnoGetOptions
@@ -157,6 +157,7 @@ export async function startBackgroundRuntime(
157
157
  config,
158
158
  {
159
159
  offline: options.offline ?? false,
160
+ indexName: options.index,
160
161
  }
161
162
  );
162
163
 
@@ -0,0 +1,136 @@
1
+ /** REST adapters for the shared Context Capsule application boundary. */
2
+
3
+ import type { GnoContextErrorCode } from "../sdk/types";
4
+ import type { ServerContext } from "./context";
5
+
6
+ import {
7
+ formatContextCapsuleMarkdown,
8
+ formatContextCapsuleVerificationMarkdown,
9
+ } from "../app/context-format";
10
+ import {
11
+ buildContextCapsule,
12
+ canonicalBuiltContextCapsuleJson,
13
+ canonicalVerifiedContextCapsuleJson,
14
+ validateContextCapsuleBuildInput,
15
+ verifyContextCapsuleRuntime,
16
+ } from "../app/context-runtime";
17
+ import {
18
+ contextSurfaceError,
19
+ parseContextBuildSurfaceInput,
20
+ parseContextVerifySurfaceInput,
21
+ } from "../app/context-surface";
22
+ import { ContextCapsuleContractError } from "../core/context-capsule";
23
+
24
+ const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
25
+ const MARKDOWN_HEADERS = {
26
+ "content-type": "text/markdown; charset=utf-8",
27
+ };
28
+
29
+ const parseJsonBody = async (request: Request): Promise<unknown> => {
30
+ try {
31
+ return await request.json();
32
+ } catch {
33
+ throw new ContextCapsuleContractError("invalid_input", "Invalid JSON body");
34
+ }
35
+ };
36
+
37
+ export type ContextRestErrorCode = GnoContextErrorCode | "runtime_error";
38
+
39
+ export const CONTEXT_REST_ERROR_STATUS = {
40
+ invalid_goal: 400,
41
+ invalid_budget: 400,
42
+ invalid_filter: 400,
43
+ invalid_uri: 400,
44
+ invalid_input: 400,
45
+ identity_mismatch: 400,
46
+ no_evidence: 404,
47
+ tokenizer_unavailable: 503,
48
+ chunk_coordinate_mismatch: 409,
49
+ stored_provenance_mismatch: 409,
50
+ index_snapshot_mismatch: 409,
51
+ index_changed_during_compile: 409,
52
+ context_changed_during_compile: 409,
53
+ capsule_mutated_during_verify: 409,
54
+ context_changed_during_verify: 409,
55
+ index_changed_during_verify: 409,
56
+ retrieval_failed: 500,
57
+ chunk_load_failed: 500,
58
+ collection_load_failed: 500,
59
+ content_load_failed: 500,
60
+ context_load_failed: 500,
61
+ document_load_failed: 500,
62
+ index_snapshot_failed: 500,
63
+ runtime_error: 500,
64
+ } as const satisfies Record<ContextRestErrorCode, number>;
65
+
66
+ export const contextRestStatusForCode = (code: string): number =>
67
+ code in CONTEXT_REST_ERROR_STATUS
68
+ ? CONTEXT_REST_ERROR_STATUS[code as ContextRestErrorCode]
69
+ : 500;
70
+
71
+ const errorResponse = (error: unknown): Response => {
72
+ const publicError = contextSurfaceError(error);
73
+ return new Response(JSON.stringify({ error: publicError }), {
74
+ status: contextRestStatusForCode(publicError.code),
75
+ headers: JSON_HEADERS,
76
+ });
77
+ };
78
+
79
+ export const handleContextBuild = async (
80
+ context: ServerContext,
81
+ request: Request
82
+ ): Promise<Response> => {
83
+ try {
84
+ const { input, format } = parseContextBuildSurfaceInput(
85
+ await parseJsonBody(request),
86
+ context.indexName
87
+ );
88
+ validateContextCapsuleBuildInput(
89
+ input,
90
+ context.indexName,
91
+ context.config.collections.map((collection) => collection.name)
92
+ );
93
+ const capsule = await buildContextCapsule(input, {
94
+ store: context.store,
95
+ config: context.config,
96
+ indexName: context.indexName,
97
+ vectorIndex: context.vectorIndex,
98
+ embedPort: context.embedPort,
99
+ rerankPort: context.rerankPort,
100
+ });
101
+ return format === "md"
102
+ ? new Response(formatContextCapsuleMarkdown(capsule), {
103
+ headers: MARKDOWN_HEADERS,
104
+ })
105
+ : new Response(canonicalBuiltContextCapsuleJson(capsule), {
106
+ headers: JSON_HEADERS,
107
+ });
108
+ } catch (error) {
109
+ return errorResponse(error);
110
+ }
111
+ };
112
+
113
+ export const handleContextVerify = async (
114
+ context: ServerContext,
115
+ request: Request
116
+ ): Promise<Response> => {
117
+ try {
118
+ const { capsule, format } = parseContextVerifySurfaceInput(
119
+ await parseJsonBody(request)
120
+ );
121
+ const receipt = await verifyContextCapsuleRuntime(capsule, {
122
+ store: context.store,
123
+ config: context.config,
124
+ indexName: context.indexName,
125
+ });
126
+ return format === "md"
127
+ ? new Response(formatContextCapsuleVerificationMarkdown(receipt), {
128
+ headers: MARKDOWN_HEADERS,
129
+ })
130
+ : new Response(canonicalVerifiedContextCapsuleJson(receipt), {
131
+ headers: JSON_HEADERS,
132
+ });
133
+ } catch (error) {
134
+ return errorResponse(error);
135
+ }
136
+ };
@@ -19,6 +19,8 @@ import type { DocumentEventBus } from "./doc-events";
19
19
  import type { EmbedScheduler } from "./embed-scheduler";
20
20
  import type { CollectionWatchService } from "./watch-service";
21
21
 
22
+ import { DEFAULT_INDEX_NAME } from "../app/constants";
23
+ import { canonicalizeIndexName } from "../app/index-name";
22
24
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
23
25
  import { resolveDownloadPolicy } from "../llm/policy";
24
26
  import { getActivePreset } from "../llm/registry";
@@ -64,6 +66,8 @@ export function resetDownloadState(): void {
64
66
  export interface ServerContext {
65
67
  store: SqliteAdapter;
66
68
  config: Config;
69
+ /** Canonical identity of the already-open resident store. */
70
+ indexName: string;
67
71
  vectorIndex: VectorIndexPort | null;
68
72
  embedPort: EmbeddingPort | null;
69
73
  expandPort: GenerationPort | null;
@@ -82,6 +86,7 @@ export interface ServerContext {
82
86
 
83
87
  export interface CreateServerContextOptions {
84
88
  offline?: boolean;
89
+ indexName?: string;
85
90
  }
86
91
 
87
92
  /**
@@ -197,6 +202,7 @@ export async function createServerContext(
197
202
  return {
198
203
  store,
199
204
  config,
205
+ indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
200
206
  vectorIndex,
201
207
  embedPort,
202
208
  expandPort,
@@ -242,5 +248,8 @@ export async function reloadServerContext(
242
248
  options: CreateServerContextOptions = {}
243
249
  ): Promise<ServerContext> {
244
250
  await disposeServerContext(ctx);
245
- return createServerContext(ctx.store, newConfig ?? ctx.config, options);
251
+ return createServerContext(ctx.store, newConfig ?? ctx.config, {
252
+ ...options,
253
+ indexName: options.indexName ?? ctx.indexName,
254
+ });
246
255
  }
@@ -4509,6 +4509,7 @@ export async function routeApi(
4509
4509
  return handleStatus({
4510
4510
  store,
4511
4511
  config,
4512
+ indexName: "default",
4512
4513
  vectorIndex: null,
4513
4514
  embedPort: null,
4514
4515
  expandPort: null,
@@ -4544,6 +4545,7 @@ export async function routeApi(
4544
4545
  current: {
4545
4546
  config,
4546
4547
  store,
4548
+ indexName: "default",
4547
4549
  vectorIndex: null,
4548
4550
  embedPort: null,
4549
4551
  expandPort: null,
@@ -9,6 +9,7 @@
9
9
  import type { ContextHolder } from "./routes/api";
10
10
 
11
11
  import { startBackgroundRuntime } from "./background-runtime";
12
+ import { handleContextBuild, handleContextVerify } from "./context-capsule";
12
13
  import { DocumentEventBus } from "./doc-events";
13
14
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
14
15
  import homepage from "./public/index.html";
@@ -525,6 +526,28 @@ export async function startServer(
525
526
  );
526
527
  },
527
528
  },
529
+ "/api/context": {
530
+ POST: async (req: Request) => {
531
+ if (!isRequestAllowed(req, port)) {
532
+ return withSecurityHeaders(forbiddenResponse(), isDev);
533
+ }
534
+ return withSecurityHeaders(
535
+ await handleContextBuild(ctxHolder.current, req),
536
+ isDev
537
+ );
538
+ },
539
+ },
540
+ "/api/context/verify": {
541
+ POST: async (req: Request) => {
542
+ if (!isRequestAllowed(req, port)) {
543
+ return withSecurityHeaders(forbiddenResponse(), isDev);
544
+ }
545
+ return withSecurityHeaders(
546
+ await handleContextVerify(ctxHolder.current, req),
547
+ isDev
548
+ );
549
+ },
550
+ },
528
551
  "/api/query/diagnose": {
529
552
  POST: async (req: Request) => {
530
553
  if (!isRequestAllowed(req, port)) {
@@ -86,12 +86,17 @@ const WHITESPACE_REGEX = /\s+/;
86
86
  const SINGLE_LINE_QUERY_PATTERN = /[\r\n]/;
87
87
  const DOUBLE_QUOTE_PATTERN = /"/g;
88
88
  const DOC_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
89
+ const SQLITE_SAFE_PARAMETER_BATCH_SIZE = 900;
89
90
  const FTS5_FIELD_WEIGHTS = {
90
91
  filepath: 1.5,
91
92
  title: 4.0,
92
93
  body: 1.0,
93
94
  } as const;
94
95
 
96
+ const uniqueNonEmptyValues = (values: readonly string[]): string[] => [
97
+ ...new Set(values.filter((value) => value.trim().length > 0)),
98
+ ];
99
+
95
100
  /** Content-free activation snapshot query; kept exported for contract tests. */
96
101
  export const ACTIVATION_INDEX_SNAPSHOT_SQL = `SELECT
97
102
  d.id,
@@ -1384,28 +1389,38 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1384
1389
  mirrorHashes: string[]
1385
1390
  ): Promise<StoreResult<Map<string, string>>> {
1386
1391
  try {
1387
- const db = this.ensureOpen();
1388
-
1389
- if (mirrorHashes.length === 0) {
1392
+ const uniqueHashes = uniqueNonEmptyValues(mirrorHashes);
1393
+ if (uniqueHashes.length === 0) {
1390
1394
  return ok(new Map());
1391
1395
  }
1396
+ const db = this.ensureOpen();
1392
1397
 
1393
1398
  interface DbContentRow {
1394
1399
  mirror_hash: string;
1395
1400
  markdown: string;
1396
1401
  }
1397
1402
 
1398
- const placeholders = mirrorHashes.map(() => "?").join(", ");
1399
- const rows = db
1400
- .query<DbContentRow, string[]>(
1401
- `SELECT mirror_hash, markdown FROM content
1402
- WHERE mirror_hash IN (${placeholders})`
1403
- )
1404
- .all(...mirrorHashes);
1403
+ const result = new Map<string, string>();
1404
+ for (
1405
+ let offset = 0;
1406
+ offset < uniqueHashes.length;
1407
+ offset += SQLITE_SAFE_PARAMETER_BATCH_SIZE
1408
+ ) {
1409
+ const batch = uniqueHashes.slice(
1410
+ offset,
1411
+ offset + SQLITE_SAFE_PARAMETER_BATCH_SIZE
1412
+ );
1413
+ const placeholders = batch.map(() => "?").join(", ");
1414
+ const rows = db
1415
+ .query<DbContentRow, string[]>(
1416
+ `SELECT mirror_hash, markdown FROM content
1417
+ WHERE mirror_hash IN (${placeholders})`
1418
+ )
1419
+ .all(...batch);
1420
+ for (const row of rows) result.set(row.mirror_hash, row.markdown);
1421
+ }
1405
1422
 
1406
- return ok(
1407
- new Map(rows.map((row) => [row.mirror_hash, row.markdown] as const))
1408
- );
1423
+ return ok(result);
1409
1424
  } catch (cause) {
1410
1425
  return err(
1411
1426
  "QUERY_FAILED",
@@ -1493,9 +1508,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1493
1508
  }
1494
1509
 
1495
1510
  // Dedupe and filter empty strings
1496
- const uniqueHashes = [
1497
- ...new Set(mirrorHashes.filter((h) => h.trim().length > 0)),
1498
- ];
1511
+ const uniqueHashes = uniqueNonEmptyValues(mirrorHashes);
1499
1512
  if (uniqueHashes.length === 0) {
1500
1513
  return ok(new Map());
1501
1514
  }
@@ -1505,11 +1518,16 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1505
1518
 
1506
1519
  // SQLite SQLITE_LIMIT_VARIABLE_NUMBER defaults to 999
1507
1520
  // Reserve 99 for potential future filter params (collection, language, etc.)
1508
- const SQLITE_MAX_PARAMS = 900;
1509
-
1510
1521
  // Batch queries to respect SQLite parameter limit
1511
- for (let i = 0; i < uniqueHashes.length; i += SQLITE_MAX_PARAMS) {
1512
- const batch = uniqueHashes.slice(i, i + SQLITE_MAX_PARAMS);
1522
+ for (
1523
+ let i = 0;
1524
+ i < uniqueHashes.length;
1525
+ i += SQLITE_SAFE_PARAMETER_BATCH_SIZE
1526
+ ) {
1527
+ const batch = uniqueHashes.slice(
1528
+ i,
1529
+ i + SQLITE_SAFE_PARAMETER_BATCH_SIZE
1530
+ );
1513
1531
  const placeholders = batch.map(() => "?").join(",");
1514
1532
  const sql = `SELECT * FROM content_chunks
1515
1533
  WHERE mirror_hash IN (${placeholders})