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.
@@ -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.
@@ -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.7",
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.7"
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
  }
@@ -1,4 +1,4 @@
1
- export const EXTENSION_VERSION = "0.3.7";
1
+ export const EXTENSION_VERSION = "0.3.8";
2
2
  export const SUPPORTED_PI_VERSION = "0.84.3";
3
3
  export const OBSERVER_PLANNER_VERSION = "observer-model-aware-v1";
4
4
  export const PLANNER_VERSION = "managed-learned-ranking-v1";