pi-mega-compact 0.11.5 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/cache-stripes.js +8 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +13 -0
  3. package/dist/extensions/dashboard-server/api-contracts/memory-map.js +1 -0
  4. package/dist/extensions/dashboard-server/routes-cache.js +28 -0
  5. package/dist/extensions/dashboard-server/routes-game.js +3 -0
  6. package/dist/extensions/dashboard-server/routes-memory-map.js +50 -0
  7. package/dist/extensions/dashboard-server/routes-setup.js +105 -0
  8. package/dist/extensions/dashboard-server/routes.js +3 -1
  9. package/dist/extensions/dashboard-server/server.js +7 -1
  10. package/dist/extensions/mega-compact.js +5 -0
  11. package/dist/extensions/mega-config.js +5 -0
  12. package/dist/extensions/mega-events/context-handler.js +10 -2
  13. package/dist/extensions/mega-events/perf-handler.js +83 -1
  14. package/dist/extensions/mega-events/separated-prompt.js +248 -0
  15. package/dist/extensions/mega-runtime/env-loader.js +56 -0
  16. package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
  17. package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
  18. package/dist/extensions/mega-runtime/runtime.js +1 -0
  19. package/dist/extensions/mega-runtime/snapshot.js +1 -0
  20. package/dist/extensions/mega-runtime/widget.js +5 -0
  21. package/dist/src/cache-stripe-impl.js +288 -0
  22. package/dist/src/cache-stripe.js +11 -0
  23. package/dist/src/config/stopwords.js +24 -0
  24. package/dist/src/config.js +17 -0
  25. package/dist/src/memoryGraph.js +299 -0
  26. package/dist/src/monitoring.js +13 -0
  27. package/dist/src/queryExpansion.js +163 -0
  28. package/dist/src/queryReformulation/cache.js +123 -0
  29. package/dist/src/queryReformulation/rrf.js +29 -0
  30. package/dist/src/queryReformulation/tfidf.js +61 -0
  31. package/dist/src/queryReformulation/vagueness.js +33 -0
  32. package/dist/src/queryReformulation.js +225 -0
  33. package/dist/src/recall.js +136 -1
  34. package/dist/src/recallMetrics.js +258 -0
  35. package/dist/src/store/sqlite/cache-stripes.js +133 -0
  36. package/dist/src/store/sqlite/fts5-search.js +68 -0
  37. package/dist/src/store/sqlite/perf-samples.js +1 -0
  38. package/dist/src/store/sqlite/schema.js +60 -1
  39. package/dist/src/store/sqlite.js +1 -0
  40. package/dist/src/store/vectorIndex.js +29 -5
  41. package/dist/src/tiered-router/types.js +18 -0
  42. package/dist/src/tieredRouter.js +421 -0
  43. package/dist/src/vectorStore.js +8 -6
  44. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js → AchievementTiles-DR9cLGDL.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js.map → AchievementTiles-DR9cLGDL.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js → AchievementsTab-svNUgJ1e.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js.map → AchievementsTab-svNUgJ1e.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js → ConfigTab-CmmWheuW.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js.map → ConfigTab-CmmWheuW.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js → EventsTab-CFE7jLsa.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js.map → EventsTab-CFE7jLsa.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/GameTab-CJAX_th_.js +2 -0
  55. package/extensions/dashboard-client/dist/assets/{GameTab-DehUX-KE.js.map → GameTab-CJAX_th_.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js → MaintenanceTab-P4SUsvMf.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js.map → MaintenanceTab-P4SUsvMf.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +2 -0
  59. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +1 -0
  60. package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js → MetricsTab-CHQscixy.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js.map → MetricsTab-CHQscixy.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js → OverviewTab-DBs4DJdd.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js.map → OverviewTab-DBs4DJdd.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js → ReposTab-DeFz76L5.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js.map → ReposTab-DeFz76L5.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js → SessionsTab-BWNsj1OB.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js.map → SessionsTab-BWNsj1OB.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js +2 -0
  69. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js.map +1 -0
  70. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js → TimeSavedCard-CiqCOQuL.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js.map → TimeSavedCard-CiqCOQuL.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js → TopicsTab-BWWQxltv.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js.map → TopicsTab-BWWQxltv.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js → TurnsTab-CVz4f4-o.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js.map → TurnsTab-CVz4f4-o.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-4gQ7auad.js → index-DrktNOoT.js} +11 -11
  78. package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js → useSSE-Bf5LOLSi.js} +2 -2
  80. package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js.map → useSSE-Bf5LOLSi.js.map} +1 -1
  81. package/extensions/dashboard-client/dist/index.html +2 -2
  82. package/extensions/dashboard-client/src/App.tsx +24 -8
  83. package/extensions/dashboard-client/src/api/client.ts +15 -0
  84. package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +100 -0
  85. package/extensions/dashboard-client/src/components/StripeDistributionCard.tsx +110 -0
  86. package/extensions/dashboard-client/src/components/TabBar.tsx +49 -4
  87. package/extensions/dashboard-client/src/main.tsx +1 -0
  88. package/extensions/dashboard-client/src/styles/base.css +30 -0
  89. package/extensions/dashboard-client/src/styles/cache.css +167 -0
  90. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +75 -13
  91. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +440 -0
  92. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +65 -2
  93. package/extensions/dashboard-server/api-contracts/cache-stripes.ts +49 -0
  94. package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -1
  95. package/extensions/dashboard-server/api-contracts/index.ts +16 -1
  96. package/extensions/dashboard-server/api-contracts/memory-map.ts +50 -0
  97. package/extensions/dashboard-server/api-contracts/setup.ts +22 -0
  98. package/extensions/dashboard-server/routes-cache.ts +36 -1
  99. package/extensions/dashboard-server/routes-game.ts +3 -0
  100. package/extensions/dashboard-server/routes-memory-map.ts +105 -0
  101. package/extensions/dashboard-server/routes-setup.ts +112 -0
  102. package/extensions/dashboard-server/routes.ts +3 -1
  103. package/extensions/dashboard-server/server.ts +6 -0
  104. package/extensions/mega-compact.ts +5 -0
  105. package/extensions/mega-config.ts +17 -0
  106. package/extensions/mega-events/context-handler.ts +10 -2
  107. package/extensions/mega-events/perf-handler.ts +111 -1
  108. package/extensions/mega-events/separated-prompt.ts +329 -0
  109. package/extensions/mega-runtime/env-loader.ts +55 -0
  110. package/extensions/mega-runtime/helpers.ts +2 -0
  111. package/extensions/mega-runtime/reset-runtime.ts +1 -0
  112. package/extensions/mega-runtime/runtime-snapshot.ts +1 -0
  113. package/extensions/mega-runtime/runtime.ts +1 -0
  114. package/extensions/mega-runtime/snapshot.ts +6 -0
  115. package/extensions/mega-runtime/widget-types.ts +3 -0
  116. package/extensions/mega-runtime/widget.ts +5 -0
  117. package/package.json +1 -1
  118. package/src/cache-stripe-impl.ts +385 -0
  119. package/src/cache-stripe.ts +18 -0
  120. package/src/config/stopwords.ts +25 -0
  121. package/src/config.ts +26 -0
  122. package/src/memoryGraph.ts +438 -0
  123. package/src/monitoring.ts +35 -0
  124. package/src/queryExpansion.ts +206 -0
  125. package/src/queryReformulation/cache.ts +169 -0
  126. package/src/queryReformulation/rrf.ts +51 -0
  127. package/src/queryReformulation/tfidf.ts +79 -0
  128. package/src/queryReformulation/vagueness.ts +38 -0
  129. package/src/queryReformulation.ts +355 -0
  130. package/src/recall.ts +168 -4
  131. package/src/recallMetrics.ts +388 -0
  132. package/src/store/sqlite/cache-stripes.ts +196 -0
  133. package/src/store/sqlite/fts5-search.ts +90 -0
  134. package/src/store/sqlite/perf-samples.ts +3 -1
  135. package/src/store/sqlite/schema.ts +60 -1
  136. package/src/store/sqlite.ts +1 -0
  137. package/src/store/vectorIndex.ts +38 -7
  138. package/src/tiered-router/types.ts +64 -0
  139. package/src/tieredRouter.ts +489 -0
  140. package/src/vectorStore.ts +9 -5
  141. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +0 -2
  142. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +0 -1
  143. package/extensions/dashboard-client/dist/assets/GameTab-DehUX-KE.js +0 -2
  144. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +0 -2
  145. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +0 -1
  146. package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +0 -1
  147. package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +0 -1
@@ -0,0 +1,196 @@
1
+ /**
2
+ * src/store/sqlite/cache-stripes.ts — Cache stripe distribution reader (A3, PLAN_V2 Phase 4).
3
+ *
4
+ * Reads from the cache_stripes table (schema.ts) to produce stripe distribution
5
+ * stats, stability aggregates, and a composite health score. Pi-agnostic.
6
+ *
7
+ * PREVENT-002: all queries use parameterized SQL.
8
+ */
9
+ import { getStateDir } from "../../store.js";
10
+ import { openStore } from "./utils.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types (mirrors api-contracts/cache-stripes.ts but as a plain domain type).
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface StripeBucket {
17
+ readonly stripe: number;
18
+ readonly label: string;
19
+ readonly count: number;
20
+ readonly avgStability: number;
21
+ readonly minStability: number;
22
+ readonly maxStability: number;
23
+ }
24
+
25
+ export interface CacheHealthScore {
26
+ readonly score: number;
27
+ readonly label: string;
28
+ readonly dominantTier: number;
29
+ readonly churnRate: number;
30
+ }
31
+
32
+ export interface CacheStripesResult {
33
+ readonly buckets: StripeBucket[];
34
+ readonly health: CacheHealthScore;
35
+ readonly epochId: string | null;
36
+ readonly lastRefreshAt: number;
37
+ readonly totalChunks: number;
38
+ }
39
+
40
+ /** Human-readable labels for each stripe tier (0=permanent .. 4=volatile). */
41
+ const STRIPE_LABELS: Record<number, string> = {
42
+ 0: "Permanent",
43
+ 1: "Epoch",
44
+ 2: "Topic",
45
+ 3: "Thread",
46
+ 4: "Volatile",
47
+ };
48
+
49
+ /** Weight per stripe for health scoring (higher = more stable = better). */
50
+ const STRIPE_HEALTH_WEIGHTS: Record<number, number> = {
51
+ 0: 1.0,
52
+ 1: 0.8,
53
+ 2: 0.5,
54
+ 3: 0.25,
55
+ 4: 0.0,
56
+ };
57
+
58
+ /**
59
+ * Read cache stripe distribution from the database.
60
+ *
61
+ * Returns aggregated per-stripe buckets with stability stats and a composite
62
+ * health score. Returns empty buckets (all zeros) when the table is empty,
63
+ * so the dashboard always has a valid response.
64
+ */
65
+ export function readCacheStripes(
66
+ stateDir: string = getStateDir(),
67
+ ): CacheStripesResult {
68
+ const db = openStore(stateDir);
69
+
70
+ // ── Fetch per-stripe aggregates ──
71
+ const rows = db
72
+ .prepare(
73
+ `SELECT
74
+ stripe,
75
+ COUNT(*) AS count,
76
+ ROUND(AVG(stability), 4) AS avgStability,
77
+ ROUND(MIN(stability), 4) AS minStability,
78
+ ROUND(MAX(stability), 4) AS maxStability
79
+ FROM cache_stripes
80
+ GROUP BY stripe
81
+ ORDER BY stripe ASC`,
82
+ )
83
+ .all() as Array<{
84
+ stripe: number;
85
+ count: number;
86
+ avgStability: number;
87
+ minStability: number;
88
+ maxStability: number;
89
+ }>;
90
+
91
+ // ── Latest epoch ──
92
+ const epochRow = db
93
+ .prepare(
94
+ `SELECT epoch_id, MAX(assigned_at) AS last_refresh
95
+ FROM cache_stripes
96
+ WHERE epoch_id IS NOT NULL`,
97
+ )
98
+ .get() as { epoch_id: string; last_refresh: number } | undefined;
99
+
100
+ // ── Total chunk count ──
101
+ const totalRow = db
102
+ .prepare("SELECT COUNT(*) AS cnt FROM cache_stripes")
103
+ .get() as { cnt: number };
104
+
105
+ const totalChunks = totalRow?.cnt ?? 0;
106
+
107
+ // ── Build bucket array (always 5 entries, stripe 0–4) ──
108
+ const rowMap = new Map<number, (typeof rows)[number]>();
109
+ for (const r of rows) rowMap.set(r.stripe, r);
110
+
111
+ const buckets: StripeBucket[] = [];
112
+ for (let s = 0; s <= 4; s++) {
113
+ const r = rowMap.get(s);
114
+ buckets.push({
115
+ stripe: s,
116
+ label: STRIPE_LABELS[s] ?? `Stripe ${s}`,
117
+ count: r?.count ?? 0,
118
+ avgStability: r?.avgStability ?? 0,
119
+ minStability: r?.minStability ?? 0,
120
+ maxStability: r?.maxStability ?? 0,
121
+ });
122
+ }
123
+
124
+ // ── Compute health score ──
125
+ const health = computeCacheHealth(buckets, totalChunks, stateDir);
126
+
127
+ return {
128
+ buckets,
129
+ health,
130
+ epochId: epochRow?.epoch_id ?? null,
131
+ lastRefreshAt: epochRow?.last_refresh ?? 0,
132
+ totalChunks,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Compute cache health from bucket distribution.
138
+ *
139
+ * Score = weighted sum of stripe fractions (normalised 0..1).
140
+ * Dominant tier = fraction of chunks in the top 2 stripes (0+1).
141
+ * Label: >= 0.8 = good, >= 0.6 = fair, >= 0.4 = degraded, else poor.
142
+ */
143
+ function computeCacheHealth(
144
+ buckets: StripeBucket[],
145
+ total: number,
146
+ stateDir: string,
147
+ ): CacheHealthScore {
148
+ if (total === 0) {
149
+ return { score: 1, label: "good", dominantTier: 1, churnRate: 0 };
150
+ }
151
+
152
+ let weightedSum = 0;
153
+ let dominantCount = 0;
154
+
155
+ for (const b of buckets) {
156
+ const w = STRIPE_HEALTH_WEIGHTS[b.stripe] ?? 0;
157
+ weightedSum += (b.count / total) * w;
158
+ if (b.stripe <= 1) dominantCount += b.count;
159
+ }
160
+
161
+ const dominantTier = dominantCount / total;
162
+
163
+ // ── Churn rate: open a new DB connection for the churn query ──
164
+ let churnRate = 0;
165
+ try {
166
+ const churnDb = openStore(stateDir);
167
+ const churnRow = churnDb
168
+ .prepare(
169
+ `WITH
170
+ ranked AS (
171
+ SELECT chunk_id, epoch_id,
172
+ ROW_NUMBER() OVER (PARTITION BY chunk_id ORDER BY assigned_at DESC) AS rn
173
+ FROM cache_stripes
174
+ ),
175
+ cur AS (SELECT chunk_id, epoch_id FROM ranked WHERE rn = 1),
176
+ prev AS (SELECT chunk_id, epoch_id FROM ranked WHERE rn = 2)
177
+ SELECT
178
+ COUNT(*) AS total_changed
179
+ FROM cur c
180
+ JOIN prev p ON c.chunk_id = p.chunk_id AND c.epoch_id != p.epoch_id`,
181
+ )
182
+ .get() as { total_changed: number } | undefined;
183
+
184
+ if (churnRow && total > 0) {
185
+ churnRate = Math.min(1, (churnRow.total_changed ?? 0) / total);
186
+ }
187
+ } catch {
188
+ // churn unavailable — keep 0
189
+ }
190
+
191
+ const score = Math.round(weightedSum * 100) / 100;
192
+ const label =
193
+ score >= 0.8 ? "good" : score >= 0.6 ? "fair" : score >= 0.4 ? "degraded" : "poor";
194
+
195
+ return { score, label, dominantTier, churnRate };
196
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * fts5-search.ts — FTS5 trigram search helper (L1 tier in the S44 tiered router).
3
+ *
4
+ * Runs BM25-ranked trigram search against the existing `context_chunks_trgm`
5
+ * virtual table (pg_trgm-equivalent, created in schema.ts). All queries are
6
+ * parameterized (PREVENT-002). Pi-agnostic — pure SQL over a node:sqlite
7
+ * DatabaseSync handle.
8
+ *
9
+ * The virtual table stores (id UNINDEXED, normalized_text) with tokenize='trigram'.
10
+ * FTS5 trigram queries tokenize the user query into overlapping 3-grams at query
11
+ * time automatically — no manual n-gram splitting needed. The BM25 rank from
12
+ * fts5 ranks results by trigram-overlap density.
13
+ */
14
+
15
+ import type { DatabaseSync } from "node:sqlite";
16
+
17
+ export interface Fts5Hit {
18
+ /** checkpoint id (chkpt_001 etc.) from the context_chunks_trgm row. */
19
+ id: string;
20
+ /** BM25 relevance score from FTS5 (higher = better match). */
21
+ score: number;
22
+ }
23
+
24
+ /**
25
+ * Search the context_chunks_trgm FTS5 table for checkpoints whose
26
+ * `normalized_text` matches `query` via trigram similarity.
27
+ *
28
+ * @param query Raw search text (FTS5 trigram tokenizer handles n-gram splitting
29
+ * automatically; input is NOT SQL-concatenated — bound as ? param).
30
+ * @param reader A sync-reader handle against the SQLite store.
31
+ * @param limit Max results to return (default 10).
32
+ * @returns BM25-ranked hits, highest score first.
33
+ */
34
+ export function fts5Search(
35
+ query: string,
36
+ reader: DatabaseSync,
37
+ limit = 10,
38
+ ): Fts5Hit[] {
39
+ const sql = `
40
+ SELECT id, bm25(context_chunks_trgm, 0.0, 1.0) AS score
41
+ FROM context_chunks_trgm
42
+ WHERE context_chunks_trgm MATCH ?
43
+ ORDER BY score
44
+ LIMIT ?
45
+ `;
46
+ const rows = reader
47
+ .prepare(sql)
48
+ .all(query, limit) as Array<{ id: string; score: number }>;
49
+
50
+ return rows.map((r) => ({ id: r.id, score: r.score }));
51
+ }
52
+
53
+ /**
54
+ * Search with an optional session_id filter for scoped FTS5 recall.
55
+ *
56
+ * JOINs the FTS5 virtual table against the real `context_chunks` table on `id`
57
+ * so we can filter by `session_id` while still using FTS5 MATCH. The vtab
58
+ * stores `id UNINDEXED` (it's a row-key passthrough, not searchable text), so
59
+ * the join is safe and efficient via the `idx_chunks_pk` index.
60
+ *
61
+ * @param query Raw search text.
62
+ * @param reader Sync-reader handle against the SQLite store.
63
+ * @param sessionId Scope to a specific session (omit for cross-session search).
64
+ * @param limit Max results (default 10).
65
+ */
66
+ export function fts5SearchScoped(
67
+ query: string,
68
+ reader: DatabaseSync,
69
+ sessionId: string | undefined,
70
+ limit = 10,
71
+ ): Fts5Hit[] {
72
+ if (sessionId) {
73
+ const sql = `
74
+ SELECT t.id, bm25(context_chunks_trgm, 0.0, 1.0) AS score
75
+ FROM context_chunks_trgm AS t
76
+ INNER JOIN context_chunks AS c ON c.id = t.id
77
+ WHERE t.context_chunks_trgm MATCH ?
78
+ AND c.session_id = ?
79
+ ORDER BY score
80
+ LIMIT ?
81
+ `;
82
+ const rows = reader
83
+ .prepare(sql)
84
+ .all(query, sessionId, limit) as Array<{ id: string; score: number }>;
85
+ return rows.map((r) => ({ id: r.id, score: r.score }));
86
+ }
87
+
88
+ // No session filter — plain FTS5 search across all sessions.
89
+ return fts5Search(query, reader, limit);
90
+ }
@@ -29,7 +29,8 @@ export type PerfKind =
29
29
  | "cpu_user_ms"
30
30
  | "cpu_sys_ms"
31
31
  | "db_recompute_ms"
32
- | "disk_write_ms";
32
+ | "disk_write_ms"
33
+ | "cache_health";
33
34
 
34
35
  /** Allow-list of valid perf sample kinds (mirrors the table's domain). */
35
36
  export const PERF_KINDS: readonly PerfKind[] = [
@@ -44,6 +45,7 @@ export const PERF_KINDS: readonly PerfKind[] = [
44
45
  "cpu_sys_ms",
45
46
  "db_recompute_ms",
46
47
  "disk_write_ms",
48
+ "cache_health",
47
49
  ];
48
50
 
49
51
  /** A single perf sample row (as stored + returned). */
@@ -4,7 +4,7 @@
4
4
  import type { DatabaseSync } from "node:sqlite";
5
5
  import { ACHIEVEMENT_DEFS } from "../../game/scoring.js";
6
6
 
7
- const SCHEMA_VERSION = 2;
7
+ const SCHEMA_VERSION = 4;
8
8
 
9
9
  /**
10
10
  * Add `column` (with `decl`, e.g. "INTEGER") to `table` if it does not already
@@ -366,6 +366,65 @@ export function initSchema(db: DatabaseSync): void {
366
366
  );
367
367
  CREATE INDEX IF NOT EXISTS idx_perf_samples_ts ON perf_samples(ts);
368
368
  CREATE INDEX IF NOT EXISTS idx_perf_samples_kind_ts ON perf_samples(kind, ts);
369
+
370
+ -- A1 PLAN_V2 Phase 2: Message Separation tables (SCHEMA_VERSION 3).
371
+ -- Stable conversation thread: user + assistant turns ONLY. Tool results are
372
+ -- stored in tool_results to keep the conversation thread cache-predictable.
373
+ -- Parameterized queries only (PREVENT-002). CREATE TABLE IF NOT EXISTS is
374
+ -- a no-op on re-open — no migration needed.
375
+ CREATE TABLE IF NOT EXISTS conversation_thread (
376
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
377
+ conversation_id TEXT NOT NULL,
378
+ role TEXT NOT NULL CHECK(role IN ('user','assistant')),
379
+ content TEXT NOT NULL,
380
+ turn_index INTEGER NOT NULL,
381
+ timestamp INTEGER NOT NULL
382
+ );
383
+ CREATE INDEX IF NOT EXISTS idx_conv_thread_conv_turn
384
+ ON conversation_thread(conversation_id, turn_index);
385
+
386
+ -- Volatile tool results: appended at END so they never disrupt the
387
+ -- conversation thread prefix (the prompt-cache-friendly prefix).
388
+ CREATE TABLE IF NOT EXISTS tool_results (
389
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
390
+ conversation_id TEXT NOT NULL,
391
+ tool_call_id TEXT NOT NULL,
392
+ tool_result TEXT NOT NULL,
393
+ turn_index INTEGER NOT NULL,
394
+ timestamp INTEGER NOT NULL
395
+ );
396
+ CREATE INDEX IF NOT EXISTS idx_tool_results_conv_turn
397
+ ON tool_results(conversation_id, turn_index);
398
+
399
+ -- A2 PLAN_V2 Phase 3: Vector-Aware Cache Striping (SCHEMA_VERSION 4).
400
+ -- Cache stripe assignments: maps each context chunk to a cache stability
401
+ -- tier (stripe). Layer 0=permanent, 1=epoch, 2=topic, 3=thread, 4=volatile.
402
+ -- stability is the composite score (0.0-1.0) from computeStabilityScore.
403
+ -- Primary key is (chunk_id, epoch_id) so the same chunk can be reassigned
404
+ -- across epochs without conflict. See cache-stripe.ts for scoring details.
405
+ -- Parameterized queries only (PREVENT-002). IF NOT EXISTS is a no-op on re-open.
406
+ CREATE TABLE IF NOT EXISTS cache_stripes (
407
+ chunk_id TEXT NOT NULL,
408
+ stripe INTEGER NOT NULL,
409
+ stability REAL NOT NULL,
410
+ assigned_at INTEGER NOT NULL,
411
+ epoch_id TEXT,
412
+ PRIMARY KEY (chunk_id, epoch_id)
413
+ );
414
+ CREATE INDEX IF NOT EXISTS idx_cs_stripe_stability
415
+ ON cache_stripes(stripe, stability DESC);
416
+
417
+ -- Embedding computation cache: avoids re-embedding content every time
418
+ -- stability scores are recomputed across epochs. Keyed by content_hash
419
+ -- (SHA-256 64-hex-char string). Embeddings are serialized via
420
+ -- Float64Array-to-Buffer to match the embedding_blob convention.
421
+ CREATE TABLE IF NOT EXISTS embedding_cache (
422
+ content_hash TEXT PRIMARY KEY,
423
+ embedding BLOB NOT NULL,
424
+ computed_at INTEGER NOT NULL
425
+ );
426
+ CREATE INDEX IF NOT EXISTS idx_emb_cache_computed
427
+ ON embedding_cache(computed_at DESC);
369
428
  `);
370
429
  // Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
371
430
  // pre-existing table, so new columns added to context_chunks after a store was
@@ -24,4 +24,5 @@ export * from "./sqlite/game-state.js";
24
24
  export * from "./sqlite/game-scores.js";
25
25
  export * from "./sqlite/game-achievements.js";
26
26
  export * from "./sqlite/perf-samples.js";
27
+ export * from "./sqlite/cache-stripes.js";
27
28
  export * from "./sqlite/turns.js";
@@ -230,13 +230,21 @@ export async function upsertEmbedding(
230
230
  const pg = await initVectorIndex();
231
231
  if (!pg) return;
232
232
  const lit = toVectorLiteral(embedding);
233
- await pg.query(
234
- `INSERT INTO vector_index (repo_id, session_id, checkpoint_id, embedding)
235
- VALUES ($1, $2, $3, $4::vector)
236
- ON CONFLICT (repo_id, session_id, checkpoint_id)
237
- DO UPDATE SET embedding = EXCLUDED.embedding;`,
238
- [repoId, sessionId, checkpointId, lit],
233
+ const upserted = await withOpenTimeout(
234
+ pg.query(
235
+ `INSERT INTO vector_index (repo_id, session_id, checkpoint_id, embedding)
236
+ VALUES ($1, $2, $3, $4::vector)
237
+ ON CONFLICT (repo_id, session_id, checkpoint_id)
238
+ DO UPDATE SET embedding = EXCLUDED.embedding;`,
239
+ [repoId, sessionId, checkpointId, lit],
240
+ ),
241
+ (reason) => {
242
+ disabled = true;
243
+ logWarn(`upsert query timed out after ${queryTimeoutMs()}ms: ${reason}`);
244
+ },
245
+ queryTimeoutMs(),
239
246
  );
247
+ if (upserted === undefined) return; // timed out — non-fatal
240
248
  } catch (err) {
241
249
  disabled = true;
242
250
  logWarn(`upsert failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -274,7 +282,15 @@ export async function searchAsync(
274
282
  params.push(repoId);
275
283
  }
276
284
  sql += " ORDER BY embedding <=> $1::vector LIMIT $2";
277
- const res = await pg.query(sql, params);
285
+ const res = await withOpenTimeout(
286
+ pg.query(sql, params),
287
+ (reason) => {
288
+ disabled = true;
289
+ logWarn(`search query timed out after ${queryTimeoutMs()}ms: ${reason}`);
290
+ },
291
+ queryTimeoutMs(),
292
+ );
293
+ if (!res) return []; // query timed out — degrade to empty (non-fatal)
278
294
  return res.rows.map((r: any) => ({
279
295
  repoId: r.repo_id as string,
280
296
  sessionId: r.session_id as string,
@@ -300,6 +316,21 @@ function closeTimeoutMs(): number {
300
316
  return DEFAULT_CLOSE_TIMEOUT_MS;
301
317
  }
302
318
 
319
+ /** Default cap for a PGlite search query. A WASM query that never resolves would
320
+ * otherwise hang the calling turn (and node --test) forever — the catch below
321
+ * only handles thrown errors, not a stalled promise. 0 disables the guard
322
+ * (preserve original unbounded behavior). Mirrors the open/close timeouts. */
323
+ export const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
324
+
325
+ function queryTimeoutMs(): number {
326
+ const raw = process.env.MEGACOMPACT_PGLITE_QUERY_TIMEOUT_MS;
327
+ if (raw !== undefined) {
328
+ const n = Number(raw);
329
+ if (Number.isFinite(n) && n >= 0) return n;
330
+ }
331
+ return DEFAULT_QUERY_TIMEOUT_MS;
332
+ }
333
+
303
334
  /**
304
335
  * Race a promise against a timeout. If the inner promise never settles, the
305
336
  * timer fires, resolves with `undefined`, and the caller proceeds. This mirrors
@@ -0,0 +1,64 @@
1
+ /**
2
+ * tiered-router/types.ts — S44 tiered router types, interfaces, and defaults.
3
+ *
4
+ * Shell file: pure data/type definitions. Zero runtime dependencies on store
5
+ * internals. Imported by tieredRouter.ts and any consumer that needs the
6
+ * RecallResult / TieredRouterMetrics shapes.
7
+ */
8
+
9
+ import type { SearchHit } from "../vectorStore.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Public interfaces
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface TieredRecallOpts {
16
+ /** Session ID to scope search. When absent, searches cross-session. */
17
+ sessionId?: string;
18
+ /** Max number of results to return (default 10). */
19
+ k?: number;
20
+ /** Skip LRU-cache lookup (force L1 hit). */
21
+ bypassCache?: boolean;
22
+ /**
23
+ * FTS5 max BM25 score for the L1 confidence gate.
24
+ * Read from calibration.json at construction; overridable per-call.
25
+ */
26
+ overrideFts5MaxBm25?: number;
27
+ }
28
+
29
+ export interface RecallResult {
30
+ /** Checkpoint search hits, ranked by relevance. */
31
+ hits: SearchHit[];
32
+ /** Which tier produced the result. */
33
+ tier: "L0" | "L1" | "L2";
34
+ /** Per-tier latency deltas in ms (contains only tiers that ran). */
35
+ latencyMs: { l0?: number; l1?: number; l2?: number };
36
+ }
37
+
38
+ export interface TieredRouterMetrics {
39
+ l0Hits: number;
40
+ l0Misses: number;
41
+ l1Hits: number;
42
+ l1Misses: number;
43
+ l2Hits: number;
44
+ l2Misses: number;
45
+ totalQueries: number;
46
+ avgLatencyMs: { l0: number; l1: number; l2: number };
47
+ fts5MaxBm25: number | null;
48
+ uncalibrated: boolean;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Defaults
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** Default LRU cache size. */
56
+ export const DEFAULT_CACHE_SIZE = 256;
57
+
58
+ /** Default latency budgets per tier (ms). */
59
+ export const DEFAULT_BUDGET_L0_MS = 1;
60
+ export const DEFAULT_BUDGET_L1_MS = 50;
61
+ export const DEFAULT_BUDGET_L2_MS = 500;
62
+
63
+ /** Default cadence for tiered-metrics logging (every N queries). */
64
+ export const DEFAULT_LOG_CADENCE = 100;