@gmickel/gno 2.2.1 → 2.3.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/README.md +1 -1
- package/assets/skill/SKILL.md +7 -0
- package/assets/skill/cli-reference.md +16 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.2.1.zip → gno-browser-clipper-v2.3.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.3.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +21 -0
- package/spec/mcp.md +9 -0
- package/spec/output-schemas/status.schema.json +41 -0
- package/src/cli/commands/shared.ts +6 -0
- package/src/cli/commands/status.ts +9 -0
- package/src/config/chunking.ts +46 -0
- package/src/config/types.ts +6 -1
- package/src/core/chunking-status.ts +30 -0
- package/src/core/mutation-generations.ts +2 -0
- package/src/ingestion/chunking.ts +108 -0
- package/src/ingestion/record-container.ts +9 -8
- package/src/ingestion/sync-options.ts +3 -1
- package/src/ingestion/sync.ts +33 -24
- package/src/ingestion/types.ts +15 -2
- package/src/mcp/tools/status.ts +5 -0
- package/src/sdk/client.ts +1 -0
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +2 -0
- package/src/store/chunking.ts +31 -0
- package/src/store/sqlite/adapter.ts +104 -2
- package/src/store/sqlite/chunking-policy.ts +212 -0
- package/src/store/types.ts +32 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.2.1.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
|
|
|
117
117
|
|
|
118
118
|
<!-- public-truth:current-version -->
|
|
119
119
|
|
|
120
|
-
> Current source version: **v2.
|
|
120
|
+
> Current source version: **v2.3.0**. See [CHANGELOG.md](./CHANGELOG.md).
|
|
121
121
|
|
|
122
122
|
<!-- /public-truth -->
|
|
123
123
|
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -509,6 +509,13 @@ gno embed --collection travel
|
|
|
509
509
|
|
|
510
510
|
MCP `gno.sync` and `gno.capture` do NOT auto-embed. Use CLI for embedding.
|
|
511
511
|
|
|
512
|
+
Optional index-wide YAML `chunking.maxTokens` and `chunking.overlapPercent`
|
|
513
|
+
control size and overlap (`0.15` means 15%). Leave defaults unless asked to
|
|
514
|
+
tune them. After a change, `gno index` rechunks cached mirrors and embeds;
|
|
515
|
+
`gno update` rechunks only. Check `gno status --json` fields
|
|
516
|
+
`chunking.pendingMirrors` and `embeddingBacklog`. Use separate `--config`
|
|
517
|
+
files and `--index` names for comparisons; see [cli-reference.md](cli-reference.md).
|
|
518
|
+
|
|
512
519
|
## Capture Notes
|
|
513
520
|
|
|
514
521
|
Use `gno capture` for quick second-brain writes into an editable collection:
|
|
@@ -24,6 +24,22 @@ punctuation. NFC/case-equivalent spellings share one identity. See
|
|
|
24
24
|
|
|
25
25
|
## Initialization
|
|
26
26
|
|
|
27
|
+
### Optional index-wide chunking
|
|
28
|
+
|
|
29
|
+
User YAML can set `chunking: { maxTokens: 256, overlapPercent: 0.15 }`.
|
|
30
|
+
Defaults remain 800 approximate tokens and 0.15 overlap; unchanged/default
|
|
31
|
+
config does not force a rebuild. The token estimate uses four characters per
|
|
32
|
+
token, and overlap accepts a fraction from 0 through 0.5.
|
|
33
|
+
|
|
34
|
+
After changing policy, `gno index` rechunks cached mirrors and embeds changed
|
|
35
|
+
chunks. `gno update` performs the rechunking without embedding. The policy
|
|
36
|
+
applies across the index even for targeted source sync; original files are not
|
|
37
|
+
read merely to rechunk. `gno status --json` reports configured/applied policy,
|
|
38
|
+
pending mirrors/documents, and a separate embedding backlog. On
|
|
39
|
+
`CHUNKING_POLICY_CONFLICT`, reopen the client with the intended configuration.
|
|
40
|
+
Compare policies with separate `--config` files and `--index` names, holding
|
|
41
|
+
corpus, models, query set, and type boosts constant.
|
|
42
|
+
|
|
27
43
|
### gno setup
|
|
28
44
|
|
|
29
45
|
Preferred activation path: add one folder, prove a real exact lexical result,
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
9eea4e537f21515ea5de8e1eaea1710b5af0a1201a8b05db399ef0b6874f5f64 gno-browser-clipper-v2.3.0.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -531,6 +531,27 @@ protection remain authoritative.
|
|
|
531
531
|
`graphHints` is active: ordered hints type
|
|
532
532
|
projected wiki/markdown edges and surface in graph traversal/diagnose metadata.
|
|
533
533
|
|
|
534
|
+
Optional root `chunking` sets one policy per index. `maxTokens` defaults to 800
|
|
535
|
+
and accepts integers from 10 through 2251799813685247; the chunker estimates
|
|
536
|
+
four characters per token. `overlapPercent` defaults to 0.15 and accepts a
|
|
537
|
+
fraction from 0 through 0.5. Missing fields inherit defaults. Invalid values
|
|
538
|
+
fail configuration validation before index mutation.
|
|
539
|
+
|
|
540
|
+
Omitted or explicitly default settings preserve existing chunks and embeddings
|
|
541
|
+
on upgrade. A policy change rechunks cached Markdown mirrors during the next
|
|
542
|
+
index/update or targeted sync, including mirrors outside the source-refresh
|
|
543
|
+
collection. Rechunking does not read original files or claim source freshness.
|
|
544
|
+
Sync-only operations leave changed chunks pending embedding; `gno index`
|
|
545
|
+
also embeds. A stale writer receives `CHUNKING_POLICY_CONFLICT` and must reopen
|
|
546
|
+
its client/runtime with the intended configuration.
|
|
547
|
+
|
|
548
|
+
Status JSON adds `chunking` with `configured` parameters, `applied` parameters
|
|
549
|
+
(null for empty/mixed layouts), `state` (`empty`, `legacy-default`, `current`,
|
|
550
|
+
`pending`, or `mixed`), `pendingDocuments`, and `pendingMirrors`. These counts
|
|
551
|
+
cover active cached content, independently of source errors and embedding
|
|
552
|
+
backlog. Sync receipts optionally add `rechunkedMirrors` when cached layouts
|
|
553
|
+
were updated; existing file counters keep their source-refresh meaning.
|
|
554
|
+
|
|
534
555
|
`collections[].sourceAvailability` is optional; omitted means `any`. Exact
|
|
535
556
|
values: `any` | `local`. There is no separate public knob beyond these two
|
|
536
557
|
modes and no claim that availability is egress policy. `any` preserves
|
package/spec/mcp.md
CHANGED
|
@@ -1352,6 +1352,15 @@ counters; it never claims attachment to another process.
|
|
|
1352
1352
|
normalized IDs/factors plus the rules fingerprint; path prefixes are never
|
|
1353
1353
|
returned.
|
|
1354
1354
|
|
|
1355
|
+
`chunking` reports the index-wide configuration and cached layout readiness:
|
|
1356
|
+
`configured` and `applied` contain `maxTokens` and `overlapPercent` (`applied`
|
|
1357
|
+
is null for empty/mixed layouts); `state` is `empty`, `legacy-default`,
|
|
1358
|
+
`current`, `pending`, or `mixed`; `pendingDocuments` and `pendingMirrors` count
|
|
1359
|
+
active cached content using another policy. Source refresh errors and embedding
|
|
1360
|
+
backlog remain separate. Reading status never rechunks or changes the policy.
|
|
1361
|
+
Sync results may include `rechunkedMirrors` when a policy change updates cached
|
|
1362
|
+
layouts. A stale client must reopen on `CHUNKING_POLICY_CONFLICT`.
|
|
1363
|
+
|
|
1355
1364
|
---
|
|
1356
1365
|
|
|
1357
1366
|
### gno_recall
|
|
@@ -24,6 +24,47 @@
|
|
|
24
24
|
"resident"
|
|
25
25
|
],
|
|
26
26
|
"properties": {
|
|
27
|
+
"chunking": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"required": [
|
|
31
|
+
"configured",
|
|
32
|
+
"applied",
|
|
33
|
+
"state",
|
|
34
|
+
"pendingDocuments",
|
|
35
|
+
"pendingMirrors"
|
|
36
|
+
],
|
|
37
|
+
"properties": {
|
|
38
|
+
"configured": { "$ref": "#/properties/chunking/definitions/params" },
|
|
39
|
+
"applied": {
|
|
40
|
+
"oneOf": [
|
|
41
|
+
{ "$ref": "#/properties/chunking/definitions/params" },
|
|
42
|
+
{ "type": "null" }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"state": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": ["empty", "legacy-default", "current", "pending", "mixed"]
|
|
48
|
+
},
|
|
49
|
+
"pendingDocuments": { "type": "integer", "minimum": 0 },
|
|
50
|
+
"pendingMirrors": { "type": "integer", "minimum": 0 }
|
|
51
|
+
},
|
|
52
|
+
"definitions": {
|
|
53
|
+
"params": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": ["maxTokens", "overlapPercent"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"maxTokens": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 10,
|
|
61
|
+
"maximum": 2251799813685247
|
|
62
|
+
},
|
|
63
|
+
"overlapPercent": { "type": "number", "minimum": 0, "maximum": 0.5 }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
27
68
|
"resident": {
|
|
28
69
|
"$ref": "gno://schemas/resident-status@1.0"
|
|
29
70
|
},
|
|
@@ -160,6 +160,12 @@ export function formatSyncResultLines(
|
|
|
160
160
|
): string[] {
|
|
161
161
|
const lines: string[] = [];
|
|
162
162
|
|
|
163
|
+
if (syncResult.rechunkedMirrors) {
|
|
164
|
+
lines.push(
|
|
165
|
+
`Rechunked ${syncResult.rechunkedMirrors} cached mirrors. Run gno embed if embedding was skipped.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
163
169
|
for (const c of syncResult.collections) {
|
|
164
170
|
lines.push(`${c.collection}:`);
|
|
165
171
|
lines.push(
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../../config";
|
|
20
20
|
import { isConnectorActivationComplete } from "../../core/activation-connector-health";
|
|
21
21
|
import { buildActivationStatus } from "../../core/activation-status";
|
|
22
|
+
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
22
23
|
import {
|
|
23
24
|
buildMemoryStatus,
|
|
24
25
|
formatMemoryStatusLines,
|
|
@@ -112,6 +113,9 @@ function formatTerminal(
|
|
|
112
113
|
lines.push(`Embedding backlog: ${indexStatus.embeddingBacklog} chunks`);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
const chunking = formatChunkingStatus(indexStatus.chunking);
|
|
117
|
+
if (chunking) lines.push(chunking);
|
|
118
|
+
|
|
115
119
|
if (indexStatus.recentErrors > 0) {
|
|
116
120
|
lines.push(`Recent errors: ${indexStatus.recentErrors} (last 24h)`);
|
|
117
121
|
}
|
|
@@ -198,6 +202,9 @@ function formatMarkdown(
|
|
|
198
202
|
lines.push(`- **Embedding backlog**: ${indexStatus.embeddingBacklog}`);
|
|
199
203
|
lines.push(`- **Recent errors**: ${indexStatus.recentErrors}`);
|
|
200
204
|
|
|
205
|
+
const chunking = formatChunkingStatus(indexStatus.chunking);
|
|
206
|
+
if (chunking) lines.push(`- ${chunking}`);
|
|
207
|
+
|
|
201
208
|
if (indexStatus.lastUpdatedAt) {
|
|
202
209
|
lines.push(`- **Last updated**: ${indexStatus.lastUpdatedAt}`);
|
|
203
210
|
}
|
|
@@ -274,6 +281,7 @@ export async function status(
|
|
|
274
281
|
try {
|
|
275
282
|
const statusResult = await store.getStatus({
|
|
276
283
|
embedModel: resolveModelUri(config, "embed"),
|
|
284
|
+
chunking: config.chunking ?? {},
|
|
277
285
|
});
|
|
278
286
|
if (!statusResult.ok) {
|
|
279
287
|
return { success: false, error: statusResult.error.message };
|
|
@@ -339,6 +347,7 @@ export function formatStatus(
|
|
|
339
347
|
totalDocuments: s.activeDocuments,
|
|
340
348
|
totalChunks: s.totalChunks,
|
|
341
349
|
embeddingBacklog: s.embeddingBacklog,
|
|
350
|
+
chunking: s.chunking,
|
|
342
351
|
lastUpdated: s.lastUpdatedAt,
|
|
343
352
|
healthy: isStatusHealthy(s, result.activation),
|
|
344
353
|
contentTypeBoost: result.contentTypeBoost,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Existing character-based chunker defaults. */
|
|
4
|
+
export const DEFAULT_CHUNKING_PARAMS = {
|
|
5
|
+
maxTokens: 800,
|
|
6
|
+
overlapPercent: 0.15,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const MAX_CHUNK_TOKENS = Math.floor(Number.MAX_SAFE_INTEGER / 4);
|
|
10
|
+
|
|
11
|
+
export const ChunkingConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
maxTokens: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(10)
|
|
17
|
+
.max(MAX_CHUNK_TOKENS)
|
|
18
|
+
.default(DEFAULT_CHUNKING_PARAMS.maxTokens),
|
|
19
|
+
overlapPercent: z
|
|
20
|
+
.number()
|
|
21
|
+
.finite()
|
|
22
|
+
.min(0)
|
|
23
|
+
.max(0.5)
|
|
24
|
+
.default(DEFAULT_CHUNKING_PARAMS.overlapPercent),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
|
|
28
|
+
export type ChunkingParams = z.infer<typeof ChunkingConfigSchema>;
|
|
29
|
+
|
|
30
|
+
export function resolveChunkingParams(
|
|
31
|
+
input?: Partial<ChunkingParams>
|
|
32
|
+
): ChunkingParams {
|
|
33
|
+
return ChunkingConfigSchema.parse(input ?? {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Canonical JSON is the policy identity, including partial/default aliases. */
|
|
37
|
+
export function chunkingPolicyKey(params: ChunkingParams): string {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
maxTokens: params.maxTokens,
|
|
40
|
+
overlapPercent: params.overlapPercent,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_CHUNKING_POLICY_KEY = chunkingPolicyKey(
|
|
45
|
+
DEFAULT_CHUNKING_PARAMS
|
|
46
|
+
);
|
package/src/config/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { URI_PREFIX } from "../app/constants";
|
|
13
13
|
import { JsonlFieldMappingSchema } from "../converters/adapters/jsonl/config";
|
|
14
14
|
import { MCP_TOOL_PROFILES } from "../mcp/tool-profile";
|
|
15
|
+
import { ChunkingConfigSchema, type ChunkingParams } from "./chunking";
|
|
15
16
|
import { RetrievalTraceConfigSchema } from "./retrieval-traces";
|
|
16
17
|
|
|
17
18
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -595,6 +596,9 @@ export const ConfigSchema = z.object({
|
|
|
595
596
|
/** Opt-in schema-lite content type rules */
|
|
596
597
|
contentTypes: z.array(ContentTypeSchema).default([]),
|
|
597
598
|
|
|
599
|
+
/** Optional index-wide chunk size/overlap; omitted preserves legacy defaults. */
|
|
600
|
+
chunking: ChunkingConfigSchema.optional(),
|
|
601
|
+
|
|
598
602
|
/** Model configuration */
|
|
599
603
|
models: ModelConfigSchema.optional(),
|
|
600
604
|
|
|
@@ -632,9 +636,10 @@ export const ConfigSchema = z.object({
|
|
|
632
636
|
|
|
633
637
|
export type Config = Omit<
|
|
634
638
|
z.infer<typeof ConfigSchema>,
|
|
635
|
-
"contentTypes" | "busyTimeoutMs"
|
|
639
|
+
"contentTypes" | "busyTimeoutMs" | "chunking"
|
|
636
640
|
> & {
|
|
637
641
|
contentTypes?: ContentTypeConfig[];
|
|
642
|
+
chunking?: Partial<ChunkingParams>;
|
|
638
643
|
/** Present after schema parse; omitted on hand-built Config objects. */
|
|
639
644
|
busyTimeoutMs?: number;
|
|
640
645
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ChunkingParams } from "../config/chunking";
|
|
2
|
+
import type { ChunkingStatus } from "../store/chunking";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
chunkingPolicyKey,
|
|
6
|
+
DEFAULT_CHUNKING_POLICY_KEY,
|
|
7
|
+
} from "../config/chunking";
|
|
8
|
+
|
|
9
|
+
const formatParams = (params: ChunkingParams): string =>
|
|
10
|
+
`${params.maxTokens} approximate tokens / ${Number((params.overlapPercent * 100).toFixed(4))}% overlap`;
|
|
11
|
+
|
|
12
|
+
/** Keep the healthy default terminal output unchanged. */
|
|
13
|
+
export function formatChunkingStatus(status?: ChunkingStatus): string | null {
|
|
14
|
+
if (!status) return null;
|
|
15
|
+
if (
|
|
16
|
+
chunkingPolicyKey(status.configured) === DEFAULT_CHUNKING_POLICY_KEY &&
|
|
17
|
+
status.pendingMirrors === 0 &&
|
|
18
|
+
status.state !== "mixed"
|
|
19
|
+
)
|
|
20
|
+
return null;
|
|
21
|
+
const applied = status.applied
|
|
22
|
+
? formatParams(status.applied)
|
|
23
|
+
: status.state === "empty"
|
|
24
|
+
? "none"
|
|
25
|
+
: "mixed";
|
|
26
|
+
const pending = status.pendingMirrors
|
|
27
|
+
? `; ${status.pendingDocuments} documents / ${status.pendingMirrors} mirrors pending (run gno update)`
|
|
28
|
+
: "";
|
|
29
|
+
return `Chunking: configured ${formatParams(status.configured)}; applied ${applied}${pending}`;
|
|
30
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Shared mutation detection for resident content and vector generations. */
|
|
2
2
|
|
|
3
3
|
interface SyncMutationCounts {
|
|
4
|
+
rechunkedMirrors?: number;
|
|
4
5
|
filesAdded?: number;
|
|
5
6
|
filesUpdated?: number;
|
|
6
7
|
filesMarkedInactive?: number;
|
|
@@ -11,6 +12,7 @@ interface SyncMutationCounts {
|
|
|
11
12
|
|
|
12
13
|
export function hasContentMutation(result: SyncMutationCounts): boolean {
|
|
13
14
|
return (
|
|
15
|
+
(result.rechunkedMirrors ?? 0) > 0 ||
|
|
14
16
|
(result.filesAdded ?? result.totalFilesAdded ?? 0) > 0 ||
|
|
15
17
|
(result.filesUpdated ?? result.totalFilesUpdated ?? 0) > 0 ||
|
|
16
18
|
(result.filesMarkedInactive ?? 0) > 0 ||
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ChunkingPolicyToken } from "../store/chunking";
|
|
2
|
+
import type { ChunkInput, StorePort, StoreResult } from "../store/types";
|
|
3
|
+
import type { ChunkerPort, SyncOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
chunkingPolicyKey,
|
|
7
|
+
DEFAULT_CHUNKING_POLICY_KEY,
|
|
8
|
+
resolveChunkingParams,
|
|
9
|
+
} from "../config/chunking";
|
|
10
|
+
import { ChunkingPolicyConflictError } from "../store/chunking";
|
|
11
|
+
|
|
12
|
+
function unwrapChunking<T>(result: StoreResult<T>): T {
|
|
13
|
+
if (result.ok) return result.value;
|
|
14
|
+
if (result.error.code === "CHUNKING_POLICY_CONFLICT") {
|
|
15
|
+
throw new ChunkingPolicyConflictError();
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Chunking failed: ${result.error.message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One preparation per sync, including nested collection/path syncs. */
|
|
21
|
+
export async function prepareChunking(
|
|
22
|
+
store: StorePort,
|
|
23
|
+
chunker: ChunkerPort,
|
|
24
|
+
options: SyncOptions
|
|
25
|
+
): Promise<{ options: SyncOptions; rechunkedMirrors: number }> {
|
|
26
|
+
if (options.chunkingToken) return { options, rechunkedMirrors: 0 };
|
|
27
|
+
const params = resolveChunkingParams(options.chunking);
|
|
28
|
+
if (
|
|
29
|
+
!(
|
|
30
|
+
store.claimChunkingPolicy &&
|
|
31
|
+
store.listPendingChunkingMirrors &&
|
|
32
|
+
store.applyChunkLayout
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
if (chunkingPolicyKey(params) !== DEFAULT_CHUNKING_POLICY_KEY) {
|
|
36
|
+
throw new Error("This store does not support configurable chunking");
|
|
37
|
+
}
|
|
38
|
+
return { options, rechunkedMirrors: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const token = unwrapChunking(await store.claimChunkingPolicy(params));
|
|
42
|
+
let rechunkedMirrors = 0;
|
|
43
|
+
let afterHash = "";
|
|
44
|
+
for (;;) {
|
|
45
|
+
const mirrors = unwrapChunking(
|
|
46
|
+
await store.listPendingChunkingMirrors(token, afterHash)
|
|
47
|
+
);
|
|
48
|
+
if (mirrors.length === 0) break;
|
|
49
|
+
for (const mirror of mirrors) {
|
|
50
|
+
const markdown = unwrapChunking(
|
|
51
|
+
await store.getContent(mirror.mirrorHash)
|
|
52
|
+
);
|
|
53
|
+
if (markdown === null)
|
|
54
|
+
throw new Error("Cached mirror disappeared during rechunking");
|
|
55
|
+
const chunks: ChunkInput[] = chunker
|
|
56
|
+
.chunk(markdown, token.params, mirror.languageHint, mirror.sourcePath)
|
|
57
|
+
.map((chunk) => ({
|
|
58
|
+
seq: chunk.seq,
|
|
59
|
+
pos: chunk.pos,
|
|
60
|
+
text: chunk.text,
|
|
61
|
+
startLine: chunk.startLine,
|
|
62
|
+
endLine: chunk.endLine,
|
|
63
|
+
language: chunk.language ?? undefined,
|
|
64
|
+
tokenCount: chunk.tokenCount ?? undefined,
|
|
65
|
+
}));
|
|
66
|
+
unwrapChunking(
|
|
67
|
+
await store.applyChunkLayout(
|
|
68
|
+
mirror.mirrorHash,
|
|
69
|
+
chunks,
|
|
70
|
+
token,
|
|
71
|
+
mirror.sourcePath,
|
|
72
|
+
mirror.languageHint
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
rechunkedMirrors += 1;
|
|
76
|
+
afterHash = mirror.mirrorHash;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
options: { ...options, chunking: token.params, chunkingToken: token },
|
|
81
|
+
rechunkedMirrors,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Preserve the original store contract for default-only test/alternate ports. */
|
|
86
|
+
export async function persistChunkLayout(
|
|
87
|
+
store: StorePort,
|
|
88
|
+
mirrorHash: string,
|
|
89
|
+
chunks: ChunkInput[],
|
|
90
|
+
token: ChunkingPolicyToken | undefined,
|
|
91
|
+
sourcePath: string,
|
|
92
|
+
languageHint?: string
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
if (token && store.applyChunkLayout) {
|
|
95
|
+
unwrapChunking(
|
|
96
|
+
await store.applyChunkLayout(
|
|
97
|
+
mirrorHash,
|
|
98
|
+
chunks,
|
|
99
|
+
token,
|
|
100
|
+
sourcePath,
|
|
101
|
+
languageHint
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
unwrapChunking(await store.upsertChunks(mirrorHash, chunks));
|
|
107
|
+
unwrapChunking(await store.rebuildFtsForHash(mirrorHash));
|
|
108
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../core/change-diff";
|
|
24
24
|
import { createEgressLineage } from "../core/egress-provenance";
|
|
25
25
|
import { normalizeTag, validateTag } from "../core/tags";
|
|
26
|
+
import { persistChunkLayout } from "./chunking";
|
|
26
27
|
import { runRecordAdapter } from "./record-adapter";
|
|
27
28
|
import { recordVirtualPath } from "./record-path";
|
|
28
29
|
import { reconcileRecordSnapshot, type RecordSyncPlan } from "./record-sync";
|
|
@@ -397,7 +398,7 @@ const persistRecord = async (
|
|
|
397
398
|
const chunks: ChunkInput[] = input.chunker
|
|
398
399
|
.chunk(
|
|
399
400
|
record.markdown,
|
|
400
|
-
DEFAULT_CHUNK_PARAMS,
|
|
401
|
+
input.options.chunkingToken?.params ?? DEFAULT_CHUNK_PARAMS,
|
|
401
402
|
record.languageHint ?? input.collection.languageHint,
|
|
402
403
|
virtualPath
|
|
403
404
|
)
|
|
@@ -410,13 +411,13 @@ const persistRecord = async (
|
|
|
410
411
|
language: chunk.language ?? undefined,
|
|
411
412
|
tokenCount: chunk.tokenCount ?? undefined,
|
|
412
413
|
}));
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
414
|
+
await persistChunkLayout(
|
|
415
|
+
input.store,
|
|
416
|
+
record.mirrorHash,
|
|
417
|
+
chunks,
|
|
418
|
+
input.options.chunkingToken,
|
|
419
|
+
virtualPath,
|
|
420
|
+
record.languageHint ?? input.collection.languageHint
|
|
420
421
|
);
|
|
421
422
|
mustOk(
|
|
422
423
|
await input.store.setDocTags(document.id, categories, "frontmatter"),
|
|
@@ -20,11 +20,13 @@ export function resolveContentTypeRules(
|
|
|
20
20
|
|
|
21
21
|
export function withContentTypeRules(
|
|
22
22
|
options: SyncOptions = {},
|
|
23
|
-
config?: Pick<Config, "contentTypes">
|
|
23
|
+
config?: Pick<Config, "contentTypes" | "chunking">
|
|
24
24
|
): SyncOptions {
|
|
25
25
|
const rules = options.contentTypeRules ?? resolveContentTypeRules(config);
|
|
26
|
+
const chunking = options.chunking ?? config?.chunking;
|
|
26
27
|
return {
|
|
27
28
|
...options,
|
|
29
|
+
...(chunking ? { chunking } : {}),
|
|
28
30
|
contentTypeRules: rules,
|
|
29
31
|
contentTypeRulesFingerprint:
|
|
30
32
|
options.contentTypeRulesFingerprint ??
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { extractMemoryScopes } from "../core/memory-record";
|
|
59
59
|
import { normalizeTag, validateTag } from "../core/tags";
|
|
60
60
|
import { defaultChunker } from "./chunker";
|
|
61
|
+
import { persistChunkLayout, prepareChunking } from "./chunking";
|
|
61
62
|
import {
|
|
62
63
|
extractHashtags,
|
|
63
64
|
parseFrontmatter,
|
|
@@ -937,7 +938,7 @@ export class SyncService {
|
|
|
937
938
|
// 9. Chunk content
|
|
938
939
|
const chunks = this.chunker.chunk(
|
|
939
940
|
artifact.markdown,
|
|
940
|
-
DEFAULT_CHUNK_PARAMS,
|
|
941
|
+
options.chunkingToken?.params ?? DEFAULT_CHUNK_PARAMS,
|
|
941
942
|
artifact.languageHint ?? collection.languageHint,
|
|
942
943
|
entry.relPath
|
|
943
944
|
);
|
|
@@ -953,21 +954,15 @@ export class SyncService {
|
|
|
953
954
|
tokenCount: c.tokenCount ?? undefined,
|
|
954
955
|
}));
|
|
955
956
|
|
|
956
|
-
// 11.
|
|
957
|
-
|
|
957
|
+
// 11-12. Apply chunks and lexical projection under the policy token.
|
|
958
|
+
await persistChunkLayout(
|
|
959
|
+
store,
|
|
958
960
|
artifact.mirrorHash,
|
|
959
|
-
chunkInputs
|
|
961
|
+
chunkInputs,
|
|
962
|
+
options.chunkingToken,
|
|
963
|
+
entry.relPath,
|
|
964
|
+
artifact.languageHint ?? collection.languageHint
|
|
960
965
|
);
|
|
961
|
-
mustOk(chunksResult, "upsertChunks", {
|
|
962
|
-
mirrorHash: artifact.mirrorHash,
|
|
963
|
-
chunkCount: chunkInputs.length,
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
// 12. Rebuild FTS for this hash - CHECKED
|
|
967
|
-
const ftsResult = await store.rebuildFtsForHash(artifact.mirrorHash);
|
|
968
|
-
mustOk(ftsResult, "rebuildFtsForHash", {
|
|
969
|
-
mirrorHash: artifact.mirrorHash,
|
|
970
|
-
});
|
|
971
966
|
|
|
972
967
|
// 13. Extract and store tags from frontmatter and body hashtags
|
|
973
968
|
// Always call setDocTags to clear removed tags on re-sync
|
|
@@ -1249,8 +1244,9 @@ export class SyncService {
|
|
|
1249
1244
|
options: SyncOptions = {}
|
|
1250
1245
|
): Promise<CollectionSyncResult> {
|
|
1251
1246
|
const startedAt = Date.now();
|
|
1247
|
+
const prepared = await prepareChunking(store, this.chunker, options);
|
|
1252
1248
|
const syncOptions: SyncOptions = {
|
|
1253
|
-
...options,
|
|
1249
|
+
...prepared.options,
|
|
1254
1250
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1255
1251
|
contentTypeRulesFingerprint:
|
|
1256
1252
|
options.contentTypeRulesFingerprint ??
|
|
@@ -1511,13 +1507,18 @@ export class SyncService {
|
|
|
1511
1507
|
syncOptions.projectTypedEdges === false
|
|
1512
1508
|
? []
|
|
1513
1509
|
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1510
|
+
return {
|
|
1511
|
+
...summarizePathResults(
|
|
1512
|
+
collection.name,
|
|
1513
|
+
results,
|
|
1514
|
+
markedInactive,
|
|
1515
|
+
startedAt,
|
|
1516
|
+
errors
|
|
1517
|
+
),
|
|
1518
|
+
...(prepared.rechunkedMirrors
|
|
1519
|
+
? { rechunkedMirrors: prepared.rechunkedMirrors }
|
|
1520
|
+
: {}),
|
|
1521
|
+
};
|
|
1521
1522
|
}
|
|
1522
1523
|
|
|
1523
1524
|
/**
|
|
@@ -1717,8 +1718,9 @@ export class SyncService {
|
|
|
1717
1718
|
options: SyncOptions = {}
|
|
1718
1719
|
): Promise<CollectionSyncResult> {
|
|
1719
1720
|
const startTime = Date.now();
|
|
1721
|
+
const prepared = await prepareChunking(store, this.chunker, options);
|
|
1720
1722
|
const syncOptions: SyncOptions = {
|
|
1721
|
-
...options,
|
|
1723
|
+
...prepared.options,
|
|
1722
1724
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1723
1725
|
contentTypeRulesFingerprint:
|
|
1724
1726
|
options.contentTypeRulesFingerprint ??
|
|
@@ -2038,6 +2040,9 @@ export class SyncService {
|
|
|
2038
2040
|
|
|
2039
2041
|
return {
|
|
2040
2042
|
collection: collection.name,
|
|
2043
|
+
...(prepared.rechunkedMirrors
|
|
2044
|
+
? { rechunkedMirrors: prepared.rechunkedMirrors }
|
|
2045
|
+
: {}),
|
|
2041
2046
|
filesProcessed: entries.length + inventoryErrored,
|
|
2042
2047
|
filesAdded: added,
|
|
2043
2048
|
filesUpdated: updated,
|
|
@@ -2060,9 +2065,10 @@ export class SyncService {
|
|
|
2060
2065
|
options: SyncOptions = {}
|
|
2061
2066
|
): Promise<SyncResult> {
|
|
2062
2067
|
const startTime = Date.now();
|
|
2068
|
+
const prepared = await prepareChunking(store, this.chunker, options);
|
|
2063
2069
|
const results: CollectionSyncResult[] = [];
|
|
2064
2070
|
const deferredProjectionOptions: SyncOptions = {
|
|
2065
|
-
...options,
|
|
2071
|
+
...prepared.options,
|
|
2066
2072
|
projectTypedEdges: false,
|
|
2067
2073
|
};
|
|
2068
2074
|
|
|
@@ -2094,6 +2100,9 @@ export class SyncService {
|
|
|
2094
2100
|
|
|
2095
2101
|
return {
|
|
2096
2102
|
collections: results,
|
|
2103
|
+
...(prepared.rechunkedMirrors
|
|
2104
|
+
? { rechunkedMirrors: prepared.rechunkedMirrors }
|
|
2105
|
+
: {}),
|
|
2097
2106
|
totalDurationMs: Date.now() - startTime,
|
|
2098
2107
|
totalFilesProcessed: totals.processed,
|
|
2099
2108
|
totalFilesAdded: totals.added,
|
package/src/ingestion/types.ts
CHANGED
|
@@ -12,8 +12,14 @@ import type {
|
|
|
12
12
|
RecordAttachmentInventoryItem,
|
|
13
13
|
} from "../converters/types";
|
|
14
14
|
import type { EgressLineage } from "../core/egress-provenance";
|
|
15
|
+
import type { ChunkingPolicyToken } from "../store/chunking";
|
|
15
16
|
import type { DirectoryAvailabilityPort } from "./source-availability/types";
|
|
16
17
|
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_CHUNKING_PARAMS,
|
|
20
|
+
type ChunkingParams,
|
|
21
|
+
} from "../config/chunking";
|
|
22
|
+
|
|
17
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
24
|
// Walker Types
|
|
19
25
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -115,8 +121,7 @@ export interface ChunkParams {
|
|
|
115
121
|
|
|
116
122
|
/** Default chunk params */
|
|
117
123
|
export const DEFAULT_CHUNK_PARAMS: ChunkParams = {
|
|
118
|
-
|
|
119
|
-
overlapPercent: 0.15,
|
|
124
|
+
...DEFAULT_CHUNKING_PARAMS,
|
|
120
125
|
};
|
|
121
126
|
|
|
122
127
|
/** Chunked output */
|
|
@@ -157,6 +162,10 @@ export interface ChunkerPort {
|
|
|
157
162
|
|
|
158
163
|
/** Sync options */
|
|
159
164
|
export interface SyncOptions {
|
|
165
|
+
/** Index-wide configured policy; omitted means the existing defaults. */
|
|
166
|
+
chunking?: Partial<ChunkingParams>;
|
|
167
|
+
/** Internal token passed from an outer sync; never a public CLI override. */
|
|
168
|
+
chunkingToken?: ChunkingPolicyToken;
|
|
160
169
|
/** Run git pull before scanning */
|
|
161
170
|
gitPull?: boolean;
|
|
162
171
|
/** Run collection updateCmd before scanning */
|
|
@@ -266,6 +275,8 @@ export interface FileSyncResult {
|
|
|
266
275
|
|
|
267
276
|
/** Collection sync summary */
|
|
268
277
|
export interface CollectionSyncResult {
|
|
278
|
+
/** Cached layouts updated independently of source-refresh file counters. */
|
|
279
|
+
rechunkedMirrors?: number;
|
|
269
280
|
collection: string;
|
|
270
281
|
filesProcessed: number;
|
|
271
282
|
filesAdded: number;
|
|
@@ -285,6 +296,8 @@ export interface CollectionSyncResult {
|
|
|
285
296
|
|
|
286
297
|
/** Full sync summary */
|
|
287
298
|
export interface SyncResult {
|
|
299
|
+
/** Cached layouts updated independently of source-refresh file counters. */
|
|
300
|
+
rechunkedMirrors?: number;
|
|
288
301
|
collections: CollectionSyncResult[];
|
|
289
302
|
totalDurationMs: number;
|
|
290
303
|
totalFilesProcessed: number;
|
package/src/mcp/tools/status.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { IndexStatus } from "../../store/types";
|
|
|
8
8
|
import type { ToolContext } from "../server";
|
|
9
9
|
|
|
10
10
|
import { buildContentTypeBoostStatus } from "../../config/content-types";
|
|
11
|
+
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
11
12
|
import { resolveModelUri } from "../../llm/registry";
|
|
12
13
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
13
14
|
import { runTool, type ToolResult } from "./index";
|
|
@@ -55,6 +56,9 @@ function formatStatus(status: IndexStatus): string {
|
|
|
55
56
|
lines.push(`Embedding backlog: ${status.embeddingBacklog} chunks`);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
const chunking = formatChunkingStatus(status.chunking);
|
|
60
|
+
if (chunking) lines.push(chunking);
|
|
61
|
+
|
|
58
62
|
if (status.recentErrors > 0) {
|
|
59
63
|
lines.push(`Recent errors: ${status.recentErrors} (last 24h)`);
|
|
60
64
|
}
|
|
@@ -79,6 +83,7 @@ export function handleStatus(
|
|
|
79
83
|
async () => {
|
|
80
84
|
const result = await ctx.store.getStatus({
|
|
81
85
|
embedModel: resolveModelUri(ctx.config, "embed"),
|
|
86
|
+
chunking: ctx.config.chunking ?? {},
|
|
82
87
|
});
|
|
83
88
|
if (!result.ok) {
|
|
84
89
|
throw new Error(result.error.message);
|
package/src/sdk/client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContentTypeBoostStatus } from "../config/content-types";
|
|
2
2
|
import type { ActivationStatus } from "../core/activation-status";
|
|
3
|
+
import type { ChunkingStatus } from "../store/chunking";
|
|
3
4
|
|
|
4
5
|
export type HealthCheckStatus = "ok" | "warn" | "error";
|
|
5
6
|
|
|
@@ -176,6 +177,7 @@ export interface BootstrapState {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
export interface AppStatusResponse {
|
|
180
|
+
chunking?: ChunkingStatus;
|
|
179
181
|
resident: ResidentStatus;
|
|
180
182
|
indexName: string;
|
|
181
183
|
configPath: string;
|
package/src/serve/status.ts
CHANGED
|
@@ -654,6 +654,7 @@ export async function buildAppStatus(
|
|
|
654
654
|
): Promise<AppStatusResponse> {
|
|
655
655
|
const result = await ctx.store.getStatus({
|
|
656
656
|
embedModel: resolveModelUri(ctx.config, "embed"),
|
|
657
|
+
chunking: ctx.config.chunking ?? {},
|
|
657
658
|
});
|
|
658
659
|
if (!result.ok) {
|
|
659
660
|
throw result.error;
|
|
@@ -733,6 +734,7 @@ export async function buildAppStatus(
|
|
|
733
734
|
totalDocuments: status.activeDocuments,
|
|
734
735
|
totalChunks: status.totalChunks,
|
|
735
736
|
embeddingBacklog: status.embeddingBacklog,
|
|
737
|
+
chunking: status.chunking,
|
|
736
738
|
lastUpdated: status.lastUpdatedAt,
|
|
737
739
|
recentErrors: status.recentErrors,
|
|
738
740
|
healthy: checks.every((check) => check.status === "ok"),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChunkingParams } from "../config/chunking";
|
|
2
|
+
|
|
3
|
+
export interface ChunkingPolicyToken {
|
|
4
|
+
params: ChunkingParams;
|
|
5
|
+
generation: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PendingChunkingMirror {
|
|
9
|
+
mirrorHash: string;
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
languageHint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChunkingStatus {
|
|
15
|
+
configured: ChunkingParams;
|
|
16
|
+
applied: ChunkingParams | null;
|
|
17
|
+
state: "empty" | "legacy-default" | "current" | "pending" | "mixed";
|
|
18
|
+
pendingDocuments: number;
|
|
19
|
+
pendingMirrors: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ChunkingPolicyConflictError extends Error {
|
|
23
|
+
readonly code = "CHUNKING_POLICY_CONFLICT";
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
super(
|
|
27
|
+
"CHUNKING_POLICY_CONFLICT: The index chunking policy changed. Reopen the client or restart the resident runtime with the intended configuration."
|
|
28
|
+
);
|
|
29
|
+
this.name = "ChunkingPolicyConflictError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -113,6 +113,11 @@ import type {
|
|
|
113
113
|
import type { SqliteDbProvider } from "./types";
|
|
114
114
|
|
|
115
115
|
import { buildUri, deriveDocid, stripUriIndex } from "../../app/constants";
|
|
116
|
+
import {
|
|
117
|
+
DEFAULT_CHUNKING_PARAMS,
|
|
118
|
+
resolveChunkingParams,
|
|
119
|
+
type ChunkingParams,
|
|
120
|
+
} from "../../config/chunking";
|
|
116
121
|
import {
|
|
117
122
|
type Collection,
|
|
118
123
|
type Context,
|
|
@@ -140,6 +145,11 @@ import {
|
|
|
140
145
|
parseActivationReceipt,
|
|
141
146
|
serializeActivationReceipt,
|
|
142
147
|
} from "../activation-receipts";
|
|
148
|
+
import {
|
|
149
|
+
ChunkingPolicyConflictError,
|
|
150
|
+
type ChunkingPolicyToken,
|
|
151
|
+
type PendingChunkingMirror,
|
|
152
|
+
} from "../chunking";
|
|
143
153
|
import { getSchemaVersion, migrations, runMigrations } from "../migrations";
|
|
144
154
|
import { err, ok } from "../types";
|
|
145
155
|
import { getStoredEmbeddingFingerprint } from "../vector/freshness";
|
|
@@ -164,6 +174,15 @@ import {
|
|
|
164
174
|
purgeDocumentChanges as purgeStoredDocumentChanges,
|
|
165
175
|
snapshotDocumentChange,
|
|
166
176
|
} from "./change-journal-store";
|
|
177
|
+
import {
|
|
178
|
+
assertChunkingTarget,
|
|
179
|
+
claimChunkingTarget,
|
|
180
|
+
getChunkingStatus,
|
|
181
|
+
markChunkingApplied,
|
|
182
|
+
pendingChunkingMirrors,
|
|
183
|
+
pruneChunkingMetadata,
|
|
184
|
+
readChunkingTarget,
|
|
185
|
+
} from "./chunking-policy";
|
|
167
186
|
import {
|
|
168
187
|
appendEgressAuditReceipt as appendStoredEgressAuditReceipt,
|
|
169
188
|
appendEgressAuditReceiptWithRetention as appendStoredEgressAuditReceiptWithRetention,
|
|
@@ -535,6 +554,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
535
554
|
private shutdownFenced = false;
|
|
536
555
|
private shutdownDeadline?: number;
|
|
537
556
|
private contextGeneration = 0;
|
|
557
|
+
private chunkingGeneration = 0;
|
|
538
558
|
|
|
539
559
|
// ─────────────────────────────────────────────────────────────────────────
|
|
540
560
|
// Lifecycle
|
|
@@ -603,6 +623,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
603
623
|
return result;
|
|
604
624
|
}
|
|
605
625
|
|
|
626
|
+
this.chunkingGeneration = readChunkingTarget(this.db).generation;
|
|
606
627
|
this.contextGeneration += 1;
|
|
607
628
|
return result;
|
|
608
629
|
} catch (cause) {
|
|
@@ -2631,12 +2652,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2631
2652
|
|
|
2632
2653
|
async upsertChunks(
|
|
2633
2654
|
mirrorHash: string,
|
|
2634
|
-
chunks: ChunkInput[]
|
|
2655
|
+
chunks: ChunkInput[],
|
|
2656
|
+
policy?: ChunkingPolicyToken
|
|
2635
2657
|
): Promise<StoreResult<void>> {
|
|
2636
2658
|
try {
|
|
2637
2659
|
const db = this.ensureOpen();
|
|
2638
2660
|
|
|
2639
2661
|
const transaction = db.transaction(() => {
|
|
2662
|
+
assertChunkingTarget(
|
|
2663
|
+
db,
|
|
2664
|
+
policy ?? {
|
|
2665
|
+
params: DEFAULT_CHUNKING_PARAMS,
|
|
2666
|
+
generation: this.chunkingGeneration,
|
|
2667
|
+
}
|
|
2668
|
+
);
|
|
2640
2669
|
// Retain stable rows: DELETE cascades erase valid legacy vectors even
|
|
2641
2670
|
// when duplicate ingestion produces exactly the same embedding input.
|
|
2642
2671
|
const nextBySequence = new Map(
|
|
@@ -2693,13 +2722,83 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2693
2722
|
return ok(undefined);
|
|
2694
2723
|
} catch (cause) {
|
|
2695
2724
|
return err(
|
|
2696
|
-
|
|
2725
|
+
cause instanceof ChunkingPolicyConflictError
|
|
2726
|
+
? cause.code
|
|
2727
|
+
: "QUERY_FAILED",
|
|
2697
2728
|
cause instanceof Error ? cause.message : "Failed to upsert chunks",
|
|
2698
2729
|
cause
|
|
2699
2730
|
);
|
|
2700
2731
|
}
|
|
2701
2732
|
}
|
|
2702
2733
|
|
|
2734
|
+
async claimChunkingPolicy(
|
|
2735
|
+
input: ChunkingParams
|
|
2736
|
+
): Promise<StoreResult<ChunkingPolicyToken>> {
|
|
2737
|
+
const params = resolveChunkingParams(input);
|
|
2738
|
+
const result = await this.withTransaction(async () =>
|
|
2739
|
+
claimChunkingTarget(this.ensureOpen(), this.chunkingGeneration, params)
|
|
2740
|
+
);
|
|
2741
|
+
if (result.ok) this.chunkingGeneration = result.value.generation;
|
|
2742
|
+
else if (result.error.cause instanceof ChunkingPolicyConflictError) {
|
|
2743
|
+
return err(
|
|
2744
|
+
"CHUNKING_POLICY_CONFLICT",
|
|
2745
|
+
result.error.message,
|
|
2746
|
+
result.error.cause
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
return result;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
async listPendingChunkingMirrors(
|
|
2753
|
+
policy: ChunkingPolicyToken,
|
|
2754
|
+
afterHash = ""
|
|
2755
|
+
): Promise<StoreResult<PendingChunkingMirror[]>> {
|
|
2756
|
+
try {
|
|
2757
|
+
return ok(pendingChunkingMirrors(this.ensureOpen(), policy, afterHash));
|
|
2758
|
+
} catch (cause) {
|
|
2759
|
+
return err(
|
|
2760
|
+
cause instanceof ChunkingPolicyConflictError
|
|
2761
|
+
? cause.code
|
|
2762
|
+
: "QUERY_FAILED",
|
|
2763
|
+
cause instanceof Error
|
|
2764
|
+
? cause.message
|
|
2765
|
+
: "Failed to list pending chunk layouts",
|
|
2766
|
+
cause
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
async applyChunkLayout(
|
|
2772
|
+
mirrorHash: string,
|
|
2773
|
+
chunks: ChunkInput[],
|
|
2774
|
+
policy: ChunkingPolicyToken,
|
|
2775
|
+
sourcePath: string,
|
|
2776
|
+
languageHint?: string
|
|
2777
|
+
): Promise<StoreResult<void>> {
|
|
2778
|
+
const result = await this.withTransaction(async () => {
|
|
2779
|
+
const db = this.ensureOpen();
|
|
2780
|
+
assertChunkingTarget(db, policy);
|
|
2781
|
+
const applied = await this.upsertChunks(mirrorHash, chunks, policy);
|
|
2782
|
+
if (!applied.ok)
|
|
2783
|
+
throw applied.error.cause ?? new Error(applied.error.message);
|
|
2784
|
+
const indexed = await this.rebuildFtsForHash(mirrorHash);
|
|
2785
|
+
if (!indexed.ok)
|
|
2786
|
+
throw indexed.error.cause ?? new Error(indexed.error.message);
|
|
2787
|
+
markChunkingApplied(db, mirrorHash, policy, sourcePath, languageHint);
|
|
2788
|
+
});
|
|
2789
|
+
if (
|
|
2790
|
+
!result.ok &&
|
|
2791
|
+
result.error.cause instanceof ChunkingPolicyConflictError
|
|
2792
|
+
) {
|
|
2793
|
+
return err(
|
|
2794
|
+
"CHUNKING_POLICY_CONFLICT",
|
|
2795
|
+
result.error.message,
|
|
2796
|
+
result.error.cause
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2703
2802
|
async getChunks(mirrorHash: string): Promise<StoreResult<ChunkRow[]>> {
|
|
2704
2803
|
try {
|
|
2705
2804
|
const db = this.ensureOpen();
|
|
@@ -5690,6 +5789,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5690
5789
|
async getStatus(options?: {
|
|
5691
5790
|
embedModel?: string;
|
|
5692
5791
|
embedFingerprint?: string;
|
|
5792
|
+
chunking?: Partial<ChunkingParams>;
|
|
5693
5793
|
}): Promise<StoreResult<IndexStatus>> {
|
|
5694
5794
|
try {
|
|
5695
5795
|
const db = this.ensureOpen();
|
|
@@ -5884,6 +5984,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5884
5984
|
activeDocuments: totalsRow?.active ?? 0,
|
|
5885
5985
|
totalChunks: chunkCount,
|
|
5886
5986
|
embeddingBacklog: variantStatus?.backlog ?? backlogRow?.count ?? 0,
|
|
5987
|
+
chunking: getChunkingStatus(db, options?.chunking),
|
|
5887
5988
|
recentErrors,
|
|
5888
5989
|
lastUpdatedAt: lastUpdatedRow?.last_updated ?? null,
|
|
5889
5990
|
healthy,
|
|
@@ -5968,6 +6069,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5968
6069
|
)
|
|
5969
6070
|
`);
|
|
5970
6071
|
orphanedContent = contentResult.changes;
|
|
6072
|
+
pruneChunkingMetadata(db);
|
|
5971
6073
|
|
|
5972
6074
|
// Delete chunks for deleted content
|
|
5973
6075
|
const chunksResult = db.run(`
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChunkingPolicyToken,
|
|
5
|
+
ChunkingStatus,
|
|
6
|
+
PendingChunkingMirror,
|
|
7
|
+
} from "../chunking";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ChunkingConfigSchema,
|
|
11
|
+
chunkingPolicyKey,
|
|
12
|
+
DEFAULT_CHUNKING_PARAMS,
|
|
13
|
+
DEFAULT_CHUNKING_POLICY_KEY,
|
|
14
|
+
resolveChunkingParams,
|
|
15
|
+
type ChunkingParams,
|
|
16
|
+
} from "../../config/chunking";
|
|
17
|
+
import { ChunkingPolicyConflictError } from "../chunking";
|
|
18
|
+
|
|
19
|
+
const TARGET_KEY = "chunking_policy_v1";
|
|
20
|
+
const LAYOUT_PREFIX = "chunking_mirror_v1:";
|
|
21
|
+
|
|
22
|
+
export function readChunkingTarget(db: Database): ChunkingPolicyToken {
|
|
23
|
+
const row = db
|
|
24
|
+
.query<{ value: string }, [string]>(
|
|
25
|
+
"SELECT value FROM schema_meta WHERE key = ?"
|
|
26
|
+
)
|
|
27
|
+
.get(TARGET_KEY);
|
|
28
|
+
if (!row) return { params: { ...DEFAULT_CHUNKING_PARAMS }, generation: 0 };
|
|
29
|
+
const parsed: unknown = JSON.parse(row.value);
|
|
30
|
+
if (
|
|
31
|
+
!parsed ||
|
|
32
|
+
typeof parsed !== "object" ||
|
|
33
|
+
!("generation" in parsed) ||
|
|
34
|
+
!Number.isSafeInteger(parsed.generation) ||
|
|
35
|
+
typeof parsed.generation !== "number" ||
|
|
36
|
+
parsed.generation < 1 ||
|
|
37
|
+
!("params" in parsed)
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("Invalid stored chunking policy");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
params: ChunkingConfigSchema.parse(parsed.params),
|
|
43
|
+
generation: parsed.generation,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertChunkingTarget(
|
|
48
|
+
db: Database,
|
|
49
|
+
token: ChunkingPolicyToken
|
|
50
|
+
): void {
|
|
51
|
+
const current = readChunkingTarget(db);
|
|
52
|
+
if (
|
|
53
|
+
current.generation !== token.generation ||
|
|
54
|
+
chunkingPolicyKey(current.params) !== chunkingPolicyKey(token.params)
|
|
55
|
+
) {
|
|
56
|
+
throw new ChunkingPolicyConflictError();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Called while the adapter holds its transaction writer. */
|
|
61
|
+
export function claimChunkingTarget(
|
|
62
|
+
db: Database,
|
|
63
|
+
observedGeneration: number,
|
|
64
|
+
params: ChunkingParams
|
|
65
|
+
): ChunkingPolicyToken {
|
|
66
|
+
const current = readChunkingTarget(db);
|
|
67
|
+
if (current.generation !== observedGeneration) {
|
|
68
|
+
throw new ChunkingPolicyConflictError();
|
|
69
|
+
}
|
|
70
|
+
if (chunkingPolicyKey(current.params) === chunkingPolicyKey(params)) {
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
const generation = current.generation + 1;
|
|
74
|
+
if (!Number.isSafeInteger(generation)) {
|
|
75
|
+
throw new Error("Chunking policy generation exhausted");
|
|
76
|
+
}
|
|
77
|
+
const target = { params, generation };
|
|
78
|
+
db.run(
|
|
79
|
+
"INSERT INTO schema_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
80
|
+
[TARGET_KEY, JSON.stringify(target)]
|
|
81
|
+
);
|
|
82
|
+
return target;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Keyset paging bounds memory even for a large index. */
|
|
86
|
+
export function pendingChunkingMirrors(
|
|
87
|
+
db: Database,
|
|
88
|
+
token: ChunkingPolicyToken,
|
|
89
|
+
afterHash: string
|
|
90
|
+
): PendingChunkingMirror[] {
|
|
91
|
+
assertChunkingTarget(db, token);
|
|
92
|
+
// No policy has ever changed, so every legacy/default layout is current.
|
|
93
|
+
if (token.generation === 0) return [];
|
|
94
|
+
return db
|
|
95
|
+
.query<
|
|
96
|
+
{
|
|
97
|
+
mirror_hash: string;
|
|
98
|
+
source_path: string;
|
|
99
|
+
language_hint: string | null;
|
|
100
|
+
},
|
|
101
|
+
[string, string, string, string]
|
|
102
|
+
>(
|
|
103
|
+
`SELECT c.mirror_hash,
|
|
104
|
+
COALESCE(json_extract(m.value, '$.sourcePath'), d.rel_path) AS source_path,
|
|
105
|
+
CASE WHEN m.value IS NOT NULL
|
|
106
|
+
THEN json_extract(m.value, '$.languageHint') ELSE d.language_hint END AS language_hint
|
|
107
|
+
FROM content c JOIN documents d ON d.id = (
|
|
108
|
+
SELECT id FROM documents
|
|
109
|
+
WHERE mirror_hash = c.mirror_hash AND active = 1
|
|
110
|
+
ORDER BY collection, rel_path, id LIMIT 1
|
|
111
|
+
)
|
|
112
|
+
LEFT JOIN schema_meta m ON m.key = ? || c.mirror_hash
|
|
113
|
+
WHERE c.mirror_hash > ?
|
|
114
|
+
AND COALESCE(json_extract(m.value, '$.params'), ?) <> ?
|
|
115
|
+
ORDER BY c.mirror_hash LIMIT 64`
|
|
116
|
+
)
|
|
117
|
+
.all(
|
|
118
|
+
LAYOUT_PREFIX,
|
|
119
|
+
afterHash,
|
|
120
|
+
DEFAULT_CHUNKING_POLICY_KEY,
|
|
121
|
+
chunkingPolicyKey(token.params)
|
|
122
|
+
)
|
|
123
|
+
.map((row) => ({
|
|
124
|
+
mirrorHash: row.mirror_hash,
|
|
125
|
+
sourcePath: row.source_path,
|
|
126
|
+
languageHint: row.language_hint ?? undefined,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function markChunkingApplied(
|
|
131
|
+
db: Database,
|
|
132
|
+
mirrorHash: string,
|
|
133
|
+
token: ChunkingPolicyToken,
|
|
134
|
+
sourcePath: string,
|
|
135
|
+
languageHint?: string
|
|
136
|
+
): void {
|
|
137
|
+
const updated = db.run(
|
|
138
|
+
`INSERT INTO schema_meta (key, value)
|
|
139
|
+
SELECT ?, ? WHERE EXISTS (SELECT 1 FROM content WHERE mirror_hash = ?)
|
|
140
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
141
|
+
[
|
|
142
|
+
`${LAYOUT_PREFIX}${mirrorHash}`,
|
|
143
|
+
JSON.stringify({ params: token.params, sourcePath, languageHint }),
|
|
144
|
+
mirrorHash,
|
|
145
|
+
]
|
|
146
|
+
);
|
|
147
|
+
if (updated.changes !== 1)
|
|
148
|
+
throw new Error("Cannot mark a missing cached mirror applied");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getChunkingStatus(
|
|
152
|
+
db: Database,
|
|
153
|
+
configured?: Partial<ChunkingParams>
|
|
154
|
+
): ChunkingStatus {
|
|
155
|
+
const params = resolveChunkingParams(
|
|
156
|
+
configured ?? readChunkingTarget(db).params
|
|
157
|
+
);
|
|
158
|
+
const key = chunkingPolicyKey(params);
|
|
159
|
+
const groups = db
|
|
160
|
+
.query<
|
|
161
|
+
{ policy: string | null; documents: number; mirrors: number },
|
|
162
|
+
[string]
|
|
163
|
+
>(
|
|
164
|
+
`SELECT json_extract(m.value, '$.params') AS policy, COUNT(*) AS documents,
|
|
165
|
+
COUNT(DISTINCT c.mirror_hash) AS mirrors
|
|
166
|
+
FROM content c JOIN documents d ON d.mirror_hash = c.mirror_hash
|
|
167
|
+
LEFT JOIN schema_meta m ON m.key = ? || c.mirror_hash
|
|
168
|
+
WHERE d.active = 1 GROUP BY policy`
|
|
169
|
+
)
|
|
170
|
+
.all(LAYOUT_PREFIX);
|
|
171
|
+
const appliedPolicies = new Map<string, ChunkingParams>();
|
|
172
|
+
let pendingDocuments = 0;
|
|
173
|
+
let pendingMirrors = 0;
|
|
174
|
+
for (const group of groups) {
|
|
175
|
+
const applied = group.policy
|
|
176
|
+
? ChunkingConfigSchema.parse(JSON.parse(group.policy))
|
|
177
|
+
: { ...DEFAULT_CHUNKING_PARAMS };
|
|
178
|
+
const appliedKey = chunkingPolicyKey(applied);
|
|
179
|
+
appliedPolicies.set(appliedKey, applied);
|
|
180
|
+
if (appliedKey !== key) {
|
|
181
|
+
pendingDocuments += group.documents;
|
|
182
|
+
pendingMirrors += group.mirrors;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let state: ChunkingStatus["state"] = "current";
|
|
186
|
+
if (groups.length === 0) state = "empty";
|
|
187
|
+
else if (appliedPolicies.size > 1) state = "mixed";
|
|
188
|
+
else if (pendingMirrors > 0) state = "pending";
|
|
189
|
+
else if (groups.every((group) => group.policy === null))
|
|
190
|
+
state = "legacy-default";
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
configured: params,
|
|
194
|
+
applied:
|
|
195
|
+
appliedPolicies.size === 1
|
|
196
|
+
? (appliedPolicies.values().next().value ?? null)
|
|
197
|
+
: null,
|
|
198
|
+
state,
|
|
199
|
+
pendingDocuments,
|
|
200
|
+
pendingMirrors,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function pruneChunkingMetadata(db: Database): void {
|
|
205
|
+
db.run(
|
|
206
|
+
`DELETE FROM schema_meta WHERE key GLOB ?
|
|
207
|
+
AND NOT EXISTS (
|
|
208
|
+
SELECT 1 FROM content WHERE mirror_hash = substr(schema_meta.key, ?)
|
|
209
|
+
)`,
|
|
210
|
+
[`${LAYOUT_PREFIX}*`, LAYOUT_PREFIX.length + 1]
|
|
211
|
+
);
|
|
212
|
+
}
|
package/src/store/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module src/store/types
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ChunkingParams } from "../config/chunking";
|
|
8
9
|
import type {
|
|
9
10
|
Collection,
|
|
10
11
|
Context,
|
|
@@ -19,6 +20,11 @@ import type {
|
|
|
19
20
|
FileRefactorRecoveryReceipt,
|
|
20
21
|
FileRefactorRecoveryReceiptDraft,
|
|
21
22
|
} from "../core/file-refactor-journal";
|
|
23
|
+
import type {
|
|
24
|
+
ChunkingPolicyToken,
|
|
25
|
+
ChunkingStatus,
|
|
26
|
+
PendingChunkingMirror,
|
|
27
|
+
} from "./chunking";
|
|
22
28
|
|
|
23
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
30
|
// Error Types
|
|
@@ -42,6 +48,7 @@ export type StoreErrorCode =
|
|
|
42
48
|
| "QUERY_FAILED"
|
|
43
49
|
| "TRANSACTION_FAILED"
|
|
44
50
|
| "INVALID_INPUT"
|
|
51
|
+
| "CHUNKING_POLICY_CONFLICT"
|
|
45
52
|
| "IO_ERROR"
|
|
46
53
|
| "INTERNAL"
|
|
47
54
|
| "EGRESS_DENIED"
|
|
@@ -806,6 +813,8 @@ export interface IndexStatus {
|
|
|
806
813
|
totalChunks: number;
|
|
807
814
|
/** Chunks without embeddings */
|
|
808
815
|
embeddingBacklog: number;
|
|
816
|
+
/** Configuration and applied cached layouts; separate from source freshness. */
|
|
817
|
+
chunking?: ChunkingStatus;
|
|
809
818
|
/** Recent ingest errors (last 24h) */
|
|
810
819
|
recentErrors: number;
|
|
811
820
|
/** Last successful update timestamp (ISO 8601) */
|
|
@@ -2036,7 +2045,28 @@ export interface StorePort {
|
|
|
2036
2045
|
*/
|
|
2037
2046
|
upsertChunks(
|
|
2038
2047
|
mirrorHash: string,
|
|
2039
|
-
chunks: ChunkInput[]
|
|
2048
|
+
chunks: ChunkInput[],
|
|
2049
|
+
policy?: ChunkingPolicyToken
|
|
2050
|
+
): Promise<StoreResult<void>>;
|
|
2051
|
+
|
|
2052
|
+
/** Claim an index-wide policy against the generation observed at open. */
|
|
2053
|
+
claimChunkingPolicy?(
|
|
2054
|
+
params: ChunkingParams
|
|
2055
|
+
): Promise<StoreResult<ChunkingPolicyToken>>;
|
|
2056
|
+
|
|
2057
|
+
/** Page cached mirrors whose applied policy differs from the claimed target. */
|
|
2058
|
+
listPendingChunkingMirrors?(
|
|
2059
|
+
policy: ChunkingPolicyToken,
|
|
2060
|
+
afterHash?: string
|
|
2061
|
+
): Promise<StoreResult<PendingChunkingMirror[]>>;
|
|
2062
|
+
|
|
2063
|
+
/** Atomically replace a layout, refresh FTS and mark its actual policy. */
|
|
2064
|
+
applyChunkLayout?(
|
|
2065
|
+
mirrorHash: string,
|
|
2066
|
+
chunks: ChunkInput[],
|
|
2067
|
+
policy: ChunkingPolicyToken,
|
|
2068
|
+
sourcePath: string,
|
|
2069
|
+
languageHint?: string
|
|
2040
2070
|
): Promise<StoreResult<void>>;
|
|
2041
2071
|
|
|
2042
2072
|
/**
|
|
@@ -2288,6 +2318,7 @@ export interface StorePort {
|
|
|
2288
2318
|
getStatus(options?: {
|
|
2289
2319
|
embedModel?: string;
|
|
2290
2320
|
embedFingerprint?: string;
|
|
2321
|
+
chunking?: Partial<ChunkingParams>;
|
|
2291
2322
|
}): Promise<StoreResult<IndexStatus>>;
|
|
2292
2323
|
|
|
2293
2324
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
cf4c351a255144b6f65dd25b71096effd05c0ea93a0691fc83ee49a3166a37c2 gno-browser-clipper-v2.2.1.zip
|