ds4-context-engine 0.3.7 → 0.3.8
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.
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 063 — Resolve FTS key deletes through rowid mapping tables
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-06
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Related:** [002](002-pi-jsonl-canonical-sqlite-rebuildable.md), [058](058-bounded-manifest-storage.md)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The FTS5 shadow tables used by the session index and the project knowledge
|
|
10
|
+
index declare `entry_key` / `snippet_id` as `UNINDEXED` columns. FTS5 cannot
|
|
11
|
+
use the token vocabulary to locate a row by an `UNINDEXED` column, so every
|
|
12
|
+
`DELETE FROM …_fts WHERE entry_key = ?` is a full scan of the whole virtual
|
|
13
|
+
table, whose cost is proportional to the size of the entire FTS index.
|
|
14
|
+
|
|
15
|
+
For incremental appends the cost is small, but a session-index **rebuild**
|
|
16
|
+
(fork, path change, truncation, missing checkpoint) re-writes every entry:
|
|
17
|
+
a large session (11k entries, ~600MB database) paid thousands of full scans
|
|
18
|
+
and stalled for hours, surfacing as an indefinite hang of the agent session.
|
|
19
|
+
|
|
20
|
+
A first candidate — making `entry_key` an indexed FTS5 column and recreating
|
|
21
|
+
the tables in a migration — was benchmarked and rejected: FTS5 then treats the
|
|
22
|
+
key as searchable vocabulary, changing the `MATCH` surface (a term present
|
|
23
|
+
only in an entry key becomes a false-positive hit) and the `bm25` column
|
|
24
|
+
weight mapping of existing queries.
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
Keep the FTS table definitions byte-identical and add dedicated mapping
|
|
29
|
+
tables that derive the SQLite rowid of each FTS row:
|
|
30
|
+
|
|
31
|
+
- `entries_fts_keys(entry_key PRIMARY KEY, fts_rowid)`
|
|
32
|
+
- `project_snippets_fts_keys(snippet_id, project_path, fts_rowid)`
|
|
33
|
+
|
|
34
|
+
Deletes become `DELETE FROM …_fts WHERE rowid = <mapped>` (O(log n)) and
|
|
35
|
+
rebuild cleanups resolve stale rows through the same mapping joined on the
|
|
36
|
+
base tables. Insert paths upsert the mapping from the FTS `last_insert_rowid`
|
|
37
|
+
inside the same write transaction as the FTS insert, so mapping and index
|
|
38
|
+
cannot diverge (a crash rolls back both).
|
|
39
|
+
|
|
40
|
+
The mapping tables are purely derived, rebuildable state: migration 16
|
|
41
|
+
backfills them from the live FTS rows with one scan, in a single
|
|
42
|
+
transaction — a failure rolls back and the previous schema keeps working.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
- Per-row FTS deletes: ~0.23 ms instead of ~40 ms–2 s (cold) per row;
|
|
47
|
+
a 11k-row rebuild drops from hours to ~3 s.
|
|
48
|
+
- The FTS schema, the `MATCH` surface and the `bm25` weight mapping are
|
|
49
|
+
unchanged (verified by the migration and repository tests).
|
|
50
|
+
- Existing databases upgrade non-destructively on first open after the
|
|
51
|
+
extension update; no user data is stored in the mapping tables.
|
|
52
|
+
- Future migrations that drop/recreate FTS tables must rebuild the mapping
|
|
53
|
+
tables in the same migration.
|
package/docs/ADR/README.md
CHANGED
|
@@ -66,5 +66,6 @@ The initial decisions from the development plan are accepted:
|
|
|
66
66
|
| [060](060-optional-portable-agent-tools.md) | Opt in to edit reports, adaptive reads/results and session-owned local jobs | Accepted |
|
|
67
67
|
| [061](061-compaction-latency.md) | Bound compaction update calls, input budgets, concurrent segments and phase timings | Accepted |
|
|
68
68
|
| [062](062-cache-aware-context-planning.md) | Opt-in cache-aware tail planning using model pricing and observed cache shares | Accepted |
|
|
69
|
+
| [063](063-fts-rowid-key-mappings.md) | Resolve FTS key deletes through rowid mapping tables | Accepted |
|
|
69
70
|
|
|
70
71
|
Each decision will receive a dedicated record when implementation pressure introduces alternatives or consequences not already covered by the development plan.
|
package/docs/releases/0.3.7.md
CHANGED
|
@@ -70,6 +70,15 @@ same tail caps as 0.3.6.
|
|
|
70
70
|
- Prefix-cache simulator: 7 passed.
|
|
71
71
|
- Cache-aware runtime integration: 4 passed.
|
|
72
72
|
- Config catalog/loader validation: passed.
|
|
73
|
+
- `pack:check` from a clean HEAD snapshot: core 239 files,
|
|
74
|
+
reference-adapter 7 files, engine 91 files; consumer install clean.
|
|
75
|
+
- Published and registry-verified: `ds4-context-core` 0.3.7,
|
|
76
|
+
`ds4-context-reference-adapter` 0.3.7, `ds4-context-engine` 0.3.7
|
|
77
|
+
(`registry:check` PASS at exact version; one transient registry replica
|
|
78
|
+
miss on first attempt, confirmed present via `npm view`).
|
|
79
|
+
- Release fix `7404a9a`: root `package.json` must depend exactly on
|
|
80
|
+
`ds4-context-core@0.3.7` (caught by the `pack:check` gate on the
|
|
81
|
+
coordinated-version requirement).
|
|
73
82
|
|
|
74
83
|
## Known limits
|
|
75
84
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Release 0.3.8 — Indexed FTS key deletes via rowid mappings (schema 16)
|
|
2
|
+
|
|
3
|
+
**Version analyzed:** DS4 Context Engine `0.3.8`
|
|
4
|
+
**Commit:** (recorded after pack verification)
|
|
5
|
+
**Coordinated packages:** `ds4-context-core` 0.3.8, `ds4-context-reference-adapter` 0.3.8, `ds4-context-engine` 0.3.8
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Bugfix release: FTS deletes were full virtual-table scans (FTS5 `UNINDEXED`
|
|
10
|
+
key columns), making session-index rebuilds on large sessions stall for
|
|
11
|
+
hours. The FTS tables are now rebuilt over rowid mapping tables, turning
|
|
12
|
+
every keyed delete into an indexed lookup. No behavior, configuration or
|
|
13
|
+
search-surface change: the fix is transparent and addresses the
|
|
14
|
+
fork/resume stall on large sessions.
|
|
15
|
+
|
|
16
|
+
## Root cause
|
|
17
|
+
|
|
18
|
+
`DELETE FROM entries_fts WHERE entry_key = ?` cannot use the FTS5
|
|
19
|
+
vocabulary because `entry_key` (and `project_snippets_fts.snippet_id`) are
|
|
20
|
+
declared `UNINDEXED`. Every row removal scanned the entire FTS index.
|
|
21
|
+
A rebuild of a session with ~11k entries performed thousands of those
|
|
22
|
+
scans: measured 65k `pread64`/s against a 615MB database and ~100 full
|
|
23
|
+
index passes per ~140s, with an estimated **hours** to completion —
|
|
24
|
+
surfacing as an indefinite hang of the agent session after a session
|
|
25
|
+
fork/resume. The behavior class pre-dates 0.3.7; the 0.3.7 work sessions
|
|
26
|
+
made it visible by pushing the database into the hundreds of MB.
|
|
27
|
+
|
|
28
|
+
Indexing the keys directly inside FTS5 was benchmarked and **rejected**:
|
|
29
|
+
it would make key text part of the `MATCH` vocabulary (false positives)
|
|
30
|
+
and shift the `bm25` column weights. See ADR 063.
|
|
31
|
+
|
|
32
|
+
## Changes
|
|
33
|
+
|
|
34
|
+
- Migration **16** `fts-rowid-key-mappings`: two derived mapping tables
|
|
35
|
+
(`entries_fts_keys`, `project_snippets_fts_keys`) backfilled from the
|
|
36
|
+
live FTS rows in one scan, inside a single transaction.
|
|
37
|
+
- `SessionIndexRepository`: per-row deletes go through the mapping
|
|
38
|
+
(`DELETE … WHERE rowid = ?`); the rebuild stale-row cleanup joins the
|
|
39
|
+
mapping with `entries` and uses the session index; the mapping is
|
|
40
|
+
upserted from the FTS `last_insert_rowid` in the same transaction.
|
|
41
|
+
- `ProjectKnowledgeRepository`: same pattern for per-snippet deletes
|
|
42
|
+
(`replaceFile`) and per-project cleanup (`clearProject`).
|
|
43
|
+
- The FTS schemas, the `MATCH` queries and the `bm25` weights are
|
|
44
|
+
byte-identical to 0.3.7; search results do not change.
|
|
45
|
+
- Tests: `tests/integration/fts-rowid-keys.test.ts` covering the
|
|
46
|
+
v15 → v16 upgrade (data preservation, key ↔ rowid correctness, search
|
|
47
|
+
surface unchanged), rebuild/append/stale-cleanup consistency and the
|
|
48
|
+
snippet `replaceFile`/`clearProject` paths; version-pinned tests moved
|
|
49
|
+
to `CURRENT_SCHEMA_VERSION`.
|
|
50
|
+
|
|
51
|
+
## Compatibility and migration
|
|
52
|
+
|
|
53
|
+
- **Non-destructive by design:** the FTS indexes are derived state; the
|
|
54
|
+
migration is transactional, so on any failure the previous schema
|
|
55
|
+
remains intact and the extension keeps working with the old behavior.
|
|
56
|
+
- **No data stored in the mapping tables:** the base tables remain the
|
|
57
|
+
single source of truth.
|
|
58
|
+
- Upgrade runs automatically on first open after the extension update
|
|
59
|
+
(measured 467 ms on a 615MB database with 31k entries / 62k snippets;
|
|
60
|
+
no user-visible step required).
|
|
61
|
+
- The mapping backfill is one scan; a very large database adds only
|
|
62
|
+
seconds to the first open after update.
|
|
63
|
+
- Existing guarantees (fail-open, atomicity, privacy, pins, compaction,
|
|
64
|
+
hard limits) are unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds4-context-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Non-destructive, provider-independent context management for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"ds4-context-core": "0.3.
|
|
65
|
+
"ds4-context-core": "0.3.8"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@earendil-works/pi-ai": "0.84.3",
|
|
@@ -77,5 +77,9 @@
|
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
79
|
"node": ">=22.19.0"
|
|
80
|
+
},
|
|
81
|
+
"allowScripts": {
|
|
82
|
+
"@google/genai@1.52.0": true,
|
|
83
|
+
"protobufjs@7.6.5": true
|
|
80
84
|
}
|
|
81
85
|
}
|