@gmickel/gno 2.12.0 → 2.12.2

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/README.md CHANGED
@@ -139,7 +139,7 @@ See the [guide](docs/COMPILED-CONTEXT.md).
139
139
 
140
140
  <!-- public-truth:current-version -->
141
141
 
142
- > Current source version: **v2.12.0**. See [CHANGELOG.md](./CHANGELOG.md).
142
+ > Current source version: **v2.12.2**. See [CHANGELOG.md](./CHANGELOG.md).
143
143
 
144
144
  <!-- /public-truth -->
145
145
 
Binary file
@@ -0,0 +1 @@
1
+ 34953d40a06904ec1c0fc7c6df54aaa14cb24acb05b38c3f15d572a285f7b3fc gno-browser-clipper-v2.12.2.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "2.12.0"
24
+ "version": "2.12.2"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/mcp.md CHANGED
@@ -3105,7 +3105,7 @@ Clients should check schema version for compatibility.
3105
3105
  - DB connection kept open for server lifetime
3106
3106
  - No persistent state between tool calls
3107
3107
  - Each tool call is independent
3108
- - Server handles concurrent requests sequentially
3108
+ - Server handles concurrent tool calls sequentially; an accepted background job (`gno_sync`, `gno_embed`, `gno_index`) waits for the call in progress, then runs without holding that queue, so reads are not blocked while it runs
3109
3109
 
3110
3110
  ---
3111
3111
 
@@ -4691,8 +4691,12 @@ function wireGraphCommand(program: Command): void {
4691
4691
  );
4692
4692
  }
4693
4693
 
4694
- const maxDepth = cmdOpts.maxDepth
4695
- ? parsePositiveInt("max-depth", cmdOpts.maxDepth)
4694
+ // `gno graph` declares --max-depth for paths, so Commander hands the
4695
+ // flag to the parent command.
4696
+ const rawMaxDepth: unknown =
4697
+ cmdOpts.maxDepth ?? command.parent?.opts().maxDepth;
4698
+ const maxDepth = rawMaxDepth
4699
+ ? parsePositiveInt("max-depth", rawMaxDepth)
4696
4700
  : undefined;
4697
4701
  const maxNodes = cmdOpts.maxNodes
4698
4702
  ? parsePositiveInt("max-nodes", cmdOpts.maxNodes)
@@ -287,20 +287,12 @@ export class JobManager {
287
287
  lock: { release: () => Promise<void> }
288
288
  ): Promise<void> {
289
289
  try {
290
- const release = await this.#toolMutex.acquire();
291
- try {
292
- this.#assertAuthorizationEpoch(job);
293
- assertInferenceActive();
294
- const result = await fn(this.#jobControllers.get(job.id)!.signal);
295
- assertInferenceActive();
296
- job.status = "completed";
297
- job.result = result;
298
- } catch (e) {
299
- job.status = "failed";
300
- job.error = e instanceof Error ? e.message : String(e);
301
- } finally {
302
- release();
303
- }
290
+ await this.#admit(job);
291
+ assertInferenceActive();
292
+ const result = await fn(this.#jobControllers.get(job.id)!.signal);
293
+ assertInferenceActive();
294
+ job.status = "completed";
295
+ job.result = result;
304
296
  } catch (e) {
305
297
  job.status = "failed";
306
298
  job.error = e instanceof Error ? e.message : String(e);
@@ -378,20 +370,12 @@ export class JobManager {
378
370
  lock: { release: () => Promise<void> }
379
371
  ): Promise<void> {
380
372
  try {
381
- const release = await this.#toolMutex.acquire();
382
- try {
383
- this.#assertAuthorizationEpoch(job);
384
- assertInferenceActive();
385
- const result = await fn(this.#jobControllers.get(job.id)!.signal);
386
- assertInferenceActive();
387
- job.status = "completed";
388
- job.typedResult = result;
389
- } catch (e) {
390
- job.status = "failed";
391
- job.error = e instanceof Error ? e.message : String(e);
392
- } finally {
393
- release();
394
- }
373
+ await this.#admit(job);
374
+ assertInferenceActive();
375
+ const result = await fn(this.#jobControllers.get(job.id)!.signal);
376
+ assertInferenceActive();
377
+ job.status = "completed";
378
+ job.typedResult = result;
395
379
  } catch (e) {
396
380
  job.status = "failed";
397
381
  job.error = e instanceof Error ? e.message : String(e);
@@ -435,6 +419,20 @@ export class JobManager {
435
419
  }
436
420
  }
437
421
 
422
+ /**
423
+ * Wait for the tool call in progress, then re-check authorization. The
424
+ * mutex is released before the job's work runs, so reads are not held
425
+ * behind a long sync or embed; writers stay serialized by the write lease.
426
+ */
427
+ async #admit(job: JobRecord): Promise<void> {
428
+ const release = await this.#toolMutex.acquire();
429
+ try {
430
+ this.#assertAuthorizationEpoch(job);
431
+ } finally {
432
+ release();
433
+ }
434
+ }
435
+
438
436
  #assertAuthorizationEpoch(job: JobRecord): void {
439
437
  if (this.#jobAuthorizationEpochs.get(job.id) === this.#authorizationEpoch) {
440
438
  return;
@@ -5,6 +5,11 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="referrer" content="no-referrer" />
7
7
  <title>GNO - Local Knowledge Index</title>
8
+ <link
9
+ rel="icon"
10
+ type="image/svg+xml"
11
+ href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0IiBmaWxsPSJub25lIiA+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnbm8tZmF2aWNvbi1ncmFkaWVudCIgeDE9IjEyIiB5MT0iOCIgeDI9IjU0IiB5Mj0iNTgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjZDA4YTNjIiAvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzhhNGExYSIgLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB4PSI2IiB5PSI2IiB3aWR0aD0iNTIiIGhlaWdodD0iNTIiIHJ4PSIxNCIgZmlsbD0idXJsKCNnbm8tZmF2aWNvbi1ncmFkaWVudCkiIC8+PHBhdGggZmlsbD0iI2ZmZjhlZiIgZD0iTTM1LjYxOSAxOC42OTRjMy41NDYgMCA2LjAzIDEuNDMgNy42ODggMy41MzZsLTIuOTQ0IDIuMjk3Yy0xLjExLTEuMzk2LTIuNjMtMi40NTktNC45MzQtMi40NTktMy45MyAwLTYuNzA1IDMuMTU4LTYuNzA1IDcuMzYxIDAgNC4zMTYgMi43MTkgNy40MTggNi45MTkgNy40MTggMS43MSAwIDMuMzA1LS41MSA0LjI4NC0xLjM5NlYzMi4xaC01LjM3N3YtMy4yODloOS4xOTR2OC4wNTNjLTIuMTI2IDIuMzU0LTUuMTI2IDMuMzU0LTguNTEgMy4zNTQtNi4xOTggMC0xMC40MDctNC40MjItMTAuNDA3LTEwLjc4OSAwLTYuMzkgNC40ODMtMTAuNzM1IDEwLjc5Mi0xMC43MzVaIiAvPjwvc3ZnPg=="
12
+ />
8
13
  <link rel="stylesheet" href="./globals.built.css" />
9
14
  </head>
10
15
  <body>
@@ -5076,20 +5076,19 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5076
5076
  const edgeTypeFilterFor = (alias: string): string =>
5077
5077
  edgeType ? `AND ${alias}.edge_type = ?` : "";
5078
5078
  const candidateEdgeLimit = Math.min(frontierLimit * 4, 4_000);
5079
- const frontierCtes: string[] = [];
5080
- const frontierParams: (number | string)[] = [];
5081
- const frontierNames = ["f0"];
5082
- const allFrontiers = (): string =>
5083
- frontierNames
5084
- .map((name) => `SELECT doc_id FROM ${name}`)
5085
- .join(" UNION ");
5086
- const frontierJoinClause = (frontierName: string): string =>
5079
+ // Each level runs as its own statement over the previous frontier and
5080
+ // the visited set, passed as JSON. One statement chaining every level
5081
+ // as CTEs re-expands each earlier level at every reference, which
5082
+ // grows exponentially with depth: SQLite 3.54 refuses it from depth 3
5083
+ // ("nested too deep") and older versions fail at depth 6.
5084
+ const visitedIds = "SELECT doc_id FROM visited";
5085
+ const frontierJoinClause =
5087
5086
  direction === "out"
5088
- ? `e.src_doc_id = ${frontierName}.doc_id`
5087
+ ? "e.src_doc_id = prev.doc_id"
5089
5088
  : direction === "in"
5090
- ? `e.dst_doc_id = ${frontierName}.doc_id`
5091
- : `(e.src_doc_id = ${frontierName}.doc_id OR e.dst_doc_id = ${frontierName}.doc_id)`;
5092
- const boundedEdgePredicate = (frontierName: string): string => {
5089
+ ? "e.dst_doc_id = prev.doc_id"
5090
+ : "(e.src_doc_id = prev.doc_id OR e.dst_doc_id = prev.doc_id)";
5091
+ const boundedEdgePredicate = (): string => {
5093
5092
  if (direction === "out") {
5094
5093
  return `
5095
5094
  e.id IN (
@@ -5110,10 +5109,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5110
5109
  ) AS next_rank
5111
5110
  FROM doc_edges e2
5112
5111
  JOIN documents next_doc ON next_doc.id = e2.dst_doc_id AND next_doc.active = 1${nextDocScope}
5113
- WHERE e2.src_doc_id = ${frontierName}.doc_id
5112
+ WHERE e2.src_doc_id = prev.doc_id
5114
5113
  ${edgeTypeFilterFor("e2")}
5115
- AND instr(${frontierName}.path, printf(',%d,', e2.dst_doc_id)) = 0
5116
- AND e2.dst_doc_id NOT IN (${allFrontiers()})
5114
+ AND instr(prev.path, printf(',%d,', e2.dst_doc_id)) = 0
5115
+ AND e2.dst_doc_id NOT IN (${visitedIds})
5117
5116
  )
5118
5117
  WHERE next_rank = 1
5119
5118
  ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
@@ -5141,10 +5140,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5141
5140
  ) AS next_rank
5142
5141
  FROM doc_edges e2
5143
5142
  JOIN documents next_doc ON next_doc.id = e2.src_doc_id AND next_doc.active = 1${nextDocScope}
5144
- WHERE e2.dst_doc_id = ${frontierName}.doc_id
5143
+ WHERE e2.dst_doc_id = prev.doc_id
5145
5144
  ${edgeTypeFilterFor("e2")}
5146
- AND instr(${frontierName}.path, printf(',%d,', e2.src_doc_id)) = 0
5147
- AND e2.src_doc_id NOT IN (${allFrontiers()})
5145
+ AND instr(prev.path, printf(',%d,', e2.src_doc_id)) = 0
5146
+ AND e2.src_doc_id NOT IN (${visitedIds})
5148
5147
  )
5149
5148
  WHERE next_rank = 1
5150
5149
  ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
@@ -5172,10 +5171,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5172
5171
  ) AS next_rank
5173
5172
  FROM doc_edges e2
5174
5173
  JOIN documents next_doc ON next_doc.id = e2.dst_doc_id AND next_doc.active = 1${nextDocScope}
5175
- WHERE e2.src_doc_id = ${frontierName}.doc_id
5174
+ WHERE e2.src_doc_id = prev.doc_id
5176
5175
  ${edgeTypeFilterFor("e2")}
5177
- AND instr(${frontierName}.path, printf(',%d,', e2.dst_doc_id)) = 0
5178
- AND e2.dst_doc_id NOT IN (${allFrontiers()})
5176
+ AND instr(prev.path, printf(',%d,', e2.dst_doc_id)) = 0
5177
+ AND e2.dst_doc_id NOT IN (${visitedIds})
5179
5178
  )
5180
5179
  WHERE next_rank = 1
5181
5180
  ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
@@ -5200,10 +5199,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5200
5199
  ) AS next_rank
5201
5200
  FROM doc_edges e3
5202
5201
  JOIN documents next_doc ON next_doc.id = e3.src_doc_id AND next_doc.active = 1${nextDocScope}
5203
- WHERE e3.dst_doc_id = ${frontierName}.doc_id
5202
+ WHERE e3.dst_doc_id = prev.doc_id
5204
5203
  ${edgeTypeFilterFor("e3")}
5205
- AND instr(${frontierName}.path, printf(',%d,', e3.src_doc_id)) = 0
5206
- AND e3.src_doc_id NOT IN (${allFrontiers()})
5204
+ AND instr(prev.path, printf(',%d,', e3.src_doc_id)) = 0
5205
+ AND e3.src_doc_id NOT IN (${visitedIds})
5207
5206
  )
5208
5207
  WHERE next_rank = 1
5209
5208
  ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
@@ -5212,40 +5211,62 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5212
5211
  )
5213
5212
  )`;
5214
5213
  };
5215
- const nextExprFor = (frontierName: string): string =>
5216
- `CASE WHEN e.src_doc_id = ${frontierName}.doc_id THEN e.dst_doc_id ELSE e.src_doc_id END`;
5217
-
5218
- const boundaryCandidateDepth = maxDepth + 1;
5219
- for (let depth = 1; depth <= boundaryCandidateDepth; depth += 1) {
5220
- const previous = `f${depth - 1}`;
5221
- const raw = `c${depth}_raw`;
5222
- const deduped = `c${depth}_deduped`;
5223
- const nodeDeduped = `c${depth}_node_deduped`;
5224
- const ranked = `c${depth}_ranked`;
5225
- const candidates = `c${depth}`;
5226
- const frontier = `f${depth}`;
5227
- const nextExpr = nextExprFor(previous);
5228
- frontierCtes.push(`
5229
- ${raw} AS (
5214
+ const nextExpr =
5215
+ "CASE WHEN e.src_doc_id = prev.doc_id THEN e.dst_doc_id ELSE e.src_doc_id END";
5216
+ const levelParams: (number | string)[] = [];
5217
+ if (direction === "both") {
5218
+ if (edgeType) {
5219
+ levelParams.push(edgeType);
5220
+ }
5221
+ levelParams.push(candidateEdgeLimit);
5222
+ }
5223
+ if (edgeType) {
5224
+ levelParams.push(edgeType);
5225
+ }
5226
+ levelParams.push(candidateEdgeLimit);
5227
+ if (edgeType) {
5228
+ levelParams.push(edgeType);
5229
+ }
5230
+ levelParams.push(
5231
+ frontierLimit,
5232
+ frontierLimit,
5233
+ visitedLimit,
5234
+ frontierLimit
5235
+ );
5236
+
5237
+ const levelQuery = (depth: number) =>
5238
+ db.query<
5239
+ {
5240
+ candidate_count: number;
5241
+ has_candidates: number;
5242
+ expansion_overflow: number;
5243
+ doc_id: number | null;
5244
+ depth: number | null;
5245
+ path: string | null;
5246
+ sort_key: string | null;
5247
+ },
5248
+ (number | string)[]
5249
+ >(`
5250
+ WITH
5251
+ prev(doc_id, path) AS (
5252
+ SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]')
5253
+ FROM json_each(?)
5254
+ ),
5255
+ visited(doc_id) AS (
5256
+ SELECT value FROM json_each(?)
5257
+ ),
5258
+ raw AS (
5230
5259
  SELECT
5231
- ${previous}.doc_id AS from_doc_id,
5260
+ prev.doc_id AS from_doc_id,
5232
5261
  ${nextExpr} AS doc_id,
5233
5262
  ${depth} AS depth,
5234
- ${previous}.path || ${nextExpr} || ',' AS path,
5263
+ prev.path || ${nextExpr} || ',' AS path,
5235
5264
  printf('%06d|%s|%012d', ${depth}, e.edge_type, ${nextExpr}) AS sort_key,
5236
5265
  e.src_doc_id,
5237
- src.docid AS source_docid,
5238
- src.uri AS source_uri,
5239
- src.title AS source_title,
5240
5266
  e.dst_doc_id,
5241
- dst.docid AS target_docid,
5242
- dst.uri AS target_uri,
5243
- dst.title AS target_title,
5244
5267
  e.edge_type,
5245
- e.confidence,
5246
- e.source,
5247
5268
  row_number() OVER (
5248
- PARTITION BY ${previous}.doc_id, e.src_doc_id, e.dst_doc_id, e.edge_type
5269
+ PARTITION BY prev.doc_id, e.src_doc_id, e.dst_doc_id, e.edge_type
5249
5270
  ORDER BY
5250
5271
  CASE e.confidence
5251
5272
  WHEN 'manual' THEN 1
@@ -5258,47 +5279,47 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5258
5279
  src.docid ASC,
5259
5280
  dst.docid ASC
5260
5281
  ) AS dedup_rank
5261
- FROM ${previous}
5262
- JOIN doc_edges e ON ${frontierJoinClause(previous)}
5263
- AND ${boundedEdgePredicate(previous)}
5282
+ FROM prev
5283
+ JOIN doc_edges e ON ${frontierJoinClause}
5284
+ AND ${boundedEdgePredicate()}
5264
5285
  JOIN documents src ON src.id = e.src_doc_id AND src.active = 1
5265
5286
  JOIN documents dst ON dst.id = e.dst_doc_id AND dst.active = 1
5266
5287
  WHERE 1 = 1
5267
5288
  ${edgeTypeFilter}
5268
- AND instr(${previous}.path, printf(',%d,', ${nextExpr})) = 0
5269
- AND ${nextExpr} NOT IN (${allFrontiers()})
5289
+ AND instr(prev.path, printf(',%d,', ${nextExpr})) = 0
5290
+ AND ${nextExpr} NOT IN (${visitedIds})
5270
5291
  ),
5271
- ${deduped} AS (
5292
+ deduped AS (
5272
5293
  SELECT *
5273
- FROM ${raw}
5294
+ FROM raw
5274
5295
  WHERE dedup_rank = 1
5275
5296
  ),
5276
- ${nodeDeduped} AS (
5297
+ node_deduped AS (
5277
5298
  SELECT
5278
5299
  from_doc_id,
5279
5300
  doc_id,
5280
5301
  depth,
5281
5302
  min(path) AS path,
5282
5303
  min(sort_key) AS sort_key
5283
- FROM ${deduped}
5304
+ FROM deduped
5284
5305
  GROUP BY from_doc_id, doc_id, depth
5285
5306
  ),
5286
- ${ranked} AS (
5307
+ ranked AS (
5287
5308
  SELECT
5288
5309
  *,
5289
5310
  row_number() OVER (
5290
5311
  PARTITION BY from_doc_id
5291
5312
  ORDER BY sort_key ASC, doc_id ASC
5292
5313
  ) AS expansion_rank
5293
- FROM ${nodeDeduped}
5314
+ FROM node_deduped
5294
5315
  ),
5295
- ${candidates} AS (
5316
+ candidates AS (
5296
5317
  SELECT doc_id, depth, min(path) AS path, min(sort_key) AS sort_key
5297
- FROM ${ranked}
5318
+ FROM ranked
5298
5319
  WHERE expansion_rank <= ?
5299
5320
  GROUP BY doc_id, depth
5300
5321
  ),
5301
- ${frontier} AS (
5322
+ frontier AS (
5302
5323
  SELECT doc_id, depth, path, sort_key
5303
5324
  FROM (
5304
5325
  SELECT
@@ -5307,35 +5328,88 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5307
5328
  path,
5308
5329
  sort_key,
5309
5330
  row_number() OVER (ORDER BY sort_key ASC, doc_id ASC) AS depth_rank
5310
- FROM ${candidates}
5331
+ FROM candidates
5311
5332
  )
5312
5333
  WHERE depth_rank <= ?
5313
- AND depth_rank <= ? - (
5314
- SELECT count(*)
5315
- FROM (${allFrontiers()})
5316
- )
5317
- )`);
5318
- if (direction === "both") {
5319
- if (edgeType) {
5320
- frontierParams.push(edgeType);
5321
- }
5322
- frontierParams.push(candidateEdgeLimit);
5323
- if (edgeType) {
5324
- frontierParams.push(edgeType);
5325
- }
5326
- frontierParams.push(candidateEdgeLimit);
5327
- } else {
5328
- if (edgeType) {
5329
- frontierParams.push(edgeType);
5330
- }
5331
- frontierParams.push(candidateEdgeLimit);
5334
+ AND depth_rank <= ? - (SELECT count(*) FROM visited)
5335
+ )
5336
+ SELECT
5337
+ stats.candidate_count,
5338
+ stats.has_candidates,
5339
+ stats.expansion_overflow,
5340
+ frontier.doc_id,
5341
+ frontier.depth,
5342
+ frontier.path,
5343
+ frontier.sort_key
5344
+ FROM (
5345
+ SELECT
5346
+ (SELECT count(*) FROM candidates) AS candidate_count,
5347
+ EXISTS (SELECT 1 FROM ranked) AS has_candidates,
5348
+ EXISTS (SELECT 1 FROM ranked WHERE expansion_rank > ?) AS expansion_overflow
5349
+ ) stats
5350
+ LEFT JOIN frontier ON 1
5351
+ ORDER BY frontier.sort_key ASC, frontier.doc_id ASC`);
5352
+
5353
+ // Walk level by level; the extra level past maxDepth only reports
5354
+ // whether the depth cap cut anything off.
5355
+ type FrontierRow = {
5356
+ doc_id: number;
5357
+ depth: number;
5358
+ path: string;
5359
+ sort_key: string;
5360
+ };
5361
+ let previous: FrontierRow[] = [
5362
+ {
5363
+ doc_id: rootDocumentId,
5364
+ depth: 0,
5365
+ path: `,${rootDocumentId},`,
5366
+ sort_key: "",
5367
+ },
5368
+ ];
5369
+ const walked: FrontierRow[] = [...previous];
5370
+ const visited = new Set([rootDocumentId]);
5371
+ let visitedOverflow = false;
5372
+ let frontierOverflow = false;
5373
+ let depthOverflow = false;
5374
+ for (let depth = 1; depth <= maxDepth + 1; depth += 1) {
5375
+ const rows = levelQuery(depth).all(
5376
+ JSON.stringify(previous.map((row) => [row.doc_id, row.path])),
5377
+ JSON.stringify([...visited]),
5378
+ ...levelParams
5379
+ );
5380
+ const stats = rows[0];
5381
+ if (depth > maxDepth) {
5382
+ depthOverflow = Boolean(stats?.has_candidates);
5383
+ break;
5332
5384
  }
5333
- if (edgeType) {
5334
- frontierParams.push(edgeType);
5385
+ const candidateCount = stats?.candidate_count ?? 0;
5386
+ if (
5387
+ candidateCount > 0 &&
5388
+ candidateCount > visitedLimit - visited.size
5389
+ ) {
5390
+ visitedOverflow = true;
5335
5391
  }
5336
- frontierParams.push(frontierLimit, frontierLimit, visitedLimit);
5337
- if (depth <= maxDepth) {
5338
- frontierNames.push(frontier);
5392
+ if (
5393
+ (candidateCount > 0 && candidateCount > frontierLimit) ||
5394
+ stats?.expansion_overflow
5395
+ ) {
5396
+ frontierOverflow = true;
5397
+ }
5398
+ previous = rows.flatMap((row) =>
5399
+ row.doc_id === null
5400
+ ? []
5401
+ : [
5402
+ {
5403
+ doc_id: row.doc_id,
5404
+ depth: row.depth ?? depth,
5405
+ path: row.path ?? "",
5406
+ sort_key: row.sort_key ?? "",
5407
+ },
5408
+ ]
5409
+ );
5410
+ for (const row of previous) {
5411
+ visited.add(row.doc_id);
5412
+ walked.push(row);
5339
5413
  }
5340
5414
  }
5341
5415
 
@@ -5345,62 +5419,18 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5345
5419
  : direction === "in"
5346
5420
  ? "AND ns.depth > nt.depth"
5347
5421
  : "";
5348
- const frontierUnion = frontierNames
5349
- .map((name) => `SELECT doc_id, depth, sort_key FROM ${name}`)
5350
- .join(" UNION ALL ");
5351
- const visitedOverflowChecks = Array.from(
5352
- { length: maxDepth },
5353
- (_, index) => {
5354
- const previousFrontiers = Array.from(
5355
- { length: index + 1 },
5356
- (__, frontierIndex) => `SELECT doc_id FROM f${frontierIndex}`
5357
- ).join(" UNION ");
5358
- return `
5359
- SELECT 1 AS has_more
5360
- FROM c${index + 1}
5361
- GROUP BY 1
5362
- HAVING count(*) > ? - (
5363
- SELECT count(*)
5364
- FROM (${previousFrontiers})
5365
- )`;
5366
- }
5367
- ).join(" UNION ALL ");
5368
- const frontierOverflowChecks = Array.from(
5369
- { length: maxDepth },
5370
- (_, index) => `
5371
- SELECT 1 AS has_more
5372
- FROM c${index + 1}
5373
- GROUP BY 1
5374
- HAVING count(*) > ?
5375
- UNION ALL
5376
- SELECT 1 AS has_more
5377
- FROM c${index + 1}_ranked
5378
- WHERE expansion_rank > ?`
5379
- ).join(" UNION ALL ");
5380
- const walkCte = `
5381
- WITH RECURSIVE
5382
- f0(doc_id, depth, path, sort_key) AS (
5383
- SELECT ?, 0, printf(',%d,', ?), ''
5384
- ),
5385
- ${frontierCtes.join(",")},
5422
+ const walkedJson = JSON.stringify(
5423
+ walked.map((row) => [row.doc_id, row.depth, row.sort_key])
5424
+ );
5425
+ const nodeDepthCte = `
5386
5426
  node_depth AS (
5387
- SELECT doc_id, min(depth) AS depth, min(sort_key) AS sort_key
5388
- FROM (${frontierUnion})
5389
- GROUP BY doc_id
5390
- ),
5391
- ranked_nodes AS (
5392
5427
  SELECT
5393
- d.*,
5394
- nd.depth,
5395
- row_number() OVER (ORDER BY nd.depth ASC, nd.sort_key ASC, d.id ASC) AS global_rank,
5396
- count(*) OVER () AS total_count
5397
- FROM node_depth nd
5398
- JOIN documents d ON d.id = nd.doc_id AND d.active = 1
5399
- )
5400
- `;
5401
-
5402
- const baseParams: (number | string)[] = [];
5403
- baseParams.push(rootDocumentId, rootDocumentId, ...frontierParams);
5428
+ json_extract(value, '$[0]') AS doc_id,
5429
+ min(json_extract(value, '$[1]')) AS depth,
5430
+ min(json_extract(value, '$[2]')) AS sort_key
5431
+ FROM json_each(?)
5432
+ GROUP BY 1
5433
+ )`;
5404
5434
 
5405
5435
  const nodeRows = db
5406
5436
  .query<
@@ -5411,14 +5441,23 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5411
5441
  },
5412
5442
  (number | string)[]
5413
5443
  >(
5414
- `${walkCte}
5444
+ `WITH ${nodeDepthCte},
5445
+ ranked_nodes AS (
5446
+ SELECT
5447
+ d.*,
5448
+ nd.depth,
5449
+ row_number() OVER (ORDER BY nd.depth ASC, nd.sort_key ASC, d.id ASC) AS global_rank,
5450
+ count(*) OVER () AS total_count
5451
+ FROM node_depth nd
5452
+ JOIN documents d ON d.id = nd.doc_id AND d.active = 1
5453
+ )
5415
5454
  SELECT *
5416
5455
  FROM ranked_nodes
5417
5456
  WHERE global_rank <= ?
5418
5457
  ORDER BY depth ASC, uri ASC, docid ASC
5419
5458
  LIMIT ?`
5420
5459
  )
5421
- .all(...baseParams, nodeLimit + 1, nodeLimit + 1);
5460
+ .all(walkedJson, nodeLimit + 1, nodeLimit + 1);
5422
5461
 
5423
5462
  const returnedNodeRows = nodeRows.slice(0, nodeLimit);
5424
5463
  const returnedIds = new Set(returnedNodeRows.map((row) => row.id));
@@ -5429,41 +5468,19 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5429
5468
  truncated = true;
5430
5469
  warnings.push("maxNodes reached");
5431
5470
  }
5432
- if (totalNodes > visitedLimit) {
5433
- truncated = true;
5434
- warnings.push("visitedLimit reached");
5435
- }
5436
-
5437
- const visitedRows = db
5438
- .query<{ has_more: number }, (number | string)[]>(
5439
- `${walkCte}
5440
- SELECT 1 AS has_more
5441
- FROM (${visitedOverflowChecks})
5442
- LIMIT 1`
5443
- )
5444
- .get(...baseParams, ...Array(maxDepth).fill(visitedLimit));
5445
- if (visitedRows) {
5471
+ if (totalNodes > visitedLimit || visitedOverflow) {
5446
5472
  truncated = true;
5447
5473
  warnings.push("visitedLimit reached");
5448
5474
  }
5449
-
5450
- const frontierRows = db
5451
- .query<{ has_more: number }, (number | string)[]>(
5452
- `${walkCte}
5453
- SELECT 1 AS has_more
5454
- FROM (${frontierOverflowChecks})
5455
- LIMIT 1`
5456
- )
5457
- .get(...baseParams, ...Array(maxDepth * 2).fill(frontierLimit));
5458
- if (frontierRows) {
5475
+ if (frontierOverflow) {
5459
5476
  truncated = true;
5460
5477
  warnings.push("frontierLimit reached");
5461
5478
  }
5462
5479
 
5463
5480
  const edgeRows = db
5464
5481
  .query<DbDocEdgeRow & { traversal_depth: number }, (number | string)[]>(
5465
- `${walkCte}
5466
- , returned_edges AS (
5482
+ `WITH ${nodeDepthCte},
5483
+ returned_edges AS (
5467
5484
  SELECT
5468
5485
  e.src_doc_id,
5469
5486
  src.docid AS source_docid,
@@ -5510,22 +5527,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5510
5527
  ORDER BY traversal_depth ASC, edge_type ASC, source_docid ASC, target_docid ASC`
5511
5528
  )
5512
5529
  .all(
5513
- ...baseParams,
5530
+ walkedJson,
5514
5531
  ...returnedIds,
5515
5532
  ...returnedIds,
5516
5533
  ...(edgeType ? [edgeType] : [])
5517
5534
  );
5518
5535
 
5519
- const boundaryRows = db
5520
- .query<{ has_more: number }, (number | string)[]>(
5521
- `${walkCte}
5522
- SELECT 1 AS has_more
5523
- FROM c${boundaryCandidateDepth}_ranked
5524
- LIMIT 1`
5525
- )
5526
- .get(...baseParams);
5527
-
5528
- if (boundaryRows) {
5536
+ if (depthOverflow) {
5529
5537
  truncated = true;
5530
5538
  warnings.push("maxDepth reached");
5531
5539
  }
@@ -1 +0,0 @@
1
- af41e9b6f20acb402696b5d05f2901b50a7454669f3b9b89f44e1370004ff411 gno-browser-clipper-v2.12.0.zip