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,248 @@
1
+ /**
2
+ * extensions/mega-events/separated-prompt.ts — A1+A2 PLAN_V2 Phase 2+3
3
+ * Message Separation + Cache-Optimized Prompt Builder:
4
+ * - buildSeparatedPrompt(): Basic 4-layer message separation (Phase 2).
5
+ * - buildCacheOptimizedPrompt(): Insert Layer 2 (vector-optimized stable
6
+ * context) between summary (Layer 1) and thread (Layer 3), ordered by
7
+ * stripe stability DESC (Phase 3).
8
+ * - detectTopicShift(): Cosine-similarity-based topic-shift detection.
9
+ * - cosineSimilarity(): cosine similarity of two Float32Array embeddings.
10
+ *
11
+ * Layer order (Phase 3): 0 (system) -> 1 (summary) -> 2 (cache stripes) ->
12
+ * 3 (thread: user/assistant turns) -> 4 (tool results at tail).
13
+ *
14
+ * Feature-gated by MEGACOMPACT_CACHE_STRIPING (default OFF).
15
+ * MEGACOMPACT_MESSAGE_SEPARATION must also be ON for any layering to occur.
16
+ * Flag-OFF = byte-identical to pre-sprint — returns messages unchanged.
17
+ */
18
+ import { openStore } from "../../src/store/sqlite/utils.js";
19
+ import { getStateDir } from "../../src/store.js";
20
+ // ─── Constants ───────────────────────────────────────────────────────────────
21
+ /** Default stripe limit: top 10 most stable chunks. */
22
+ const DEFAULT_STRIPE_LIMIT = 10;
23
+ /** Topic-shift threshold: similarity below this triggers a stripe refresh. */
24
+ const TOPIC_SHIFT_THRESHOLD = 0.7;
25
+ // ─── Build separated prompt (Phase 2) ────────────────────────────────────────
26
+ /**
27
+ * Reorganize messages into stable layers for better prompt caching.
28
+ *
29
+ * Layer order: 0 (system) -> 1 (summary) -> 3 (thread: user/assistant turns) ->
30
+ * 4 (tool results at tail).
31
+ *
32
+ * Feature gate: MEGACOMPACT_MESSAGE_SEPARATION env var. When OFF (default),
33
+ * returns `messages` unchanged (byte-identical to pre-sprint).
34
+ */
35
+ export function buildSeparatedPrompt(messages, _opts) {
36
+ const flag = process.env.MEGACOMPACT_MESSAGE_SEPARATION;
37
+ if (flag === "0" || flag === "false" || flag === undefined || flag === "") {
38
+ return messages;
39
+ }
40
+ // pi's AgentMessage union has no "system" role — the system prompt lives in
41
+ // AgentState.systemPrompt, separate from this array. The cache-relevant,
42
+ // low-risk transformation is moving volatile tool results/executions to the
43
+ // tail so the stable prefix (user/assistant/summaries/custom) stays contiguous.
44
+ // Discriminate by role only — never reach into `.content`/`.tool_calls`,
45
+ // which are variant-specific and require narrowing.
46
+ const main = [];
47
+ const tail = [];
48
+ for (const m of messages) {
49
+ if (m.role === "toolResult" || m.role === "bashExecution") {
50
+ tail.push(m);
51
+ }
52
+ else {
53
+ main.push(m);
54
+ }
55
+ }
56
+ if (tail.length === 0)
57
+ return messages; // nothing to reorder — byte-identical
58
+ return [...main, ...tail];
59
+ }
60
+ // ─── Cosine similarity ───────────────────────────────────────────────────────
61
+ /**
62
+ * Compute cosine similarity between two Float32Array embeddings.
63
+ * Returns a value in [0, 1] where 1 = identical direction.
64
+ * Returns 0 if either vector is zero or inputs are mismatched.
65
+ */
66
+ export function cosineSimilarity(a, b) {
67
+ if (a.length !== b.length || a.length === 0)
68
+ return 0;
69
+ let dot = 0;
70
+ let normA = 0;
71
+ let normB = 0;
72
+ for (let i = 0; i < a.length; i++) {
73
+ dot += a[i] * b[i];
74
+ normA += a[i] * a[i];
75
+ normB += b[i] * b[i];
76
+ }
77
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
78
+ if (denom === 0)
79
+ return 0;
80
+ return dot / denom;
81
+ }
82
+ // ─── Buffer ↔ Float32Array helpers ───────────────────────────────────────────
83
+ /**
84
+ * Decode a Float32Array from a Buffer (little-endian float32 blob).
85
+ * Returns null on empty/undefined input.
86
+ */
87
+ export function decodeEmbeddingBlob(buf) {
88
+ if (!buf || buf.length === 0)
89
+ return null;
90
+ return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
91
+ }
92
+ /**
93
+ * Encode a Float32Array into a Buffer for DB storage.
94
+ */
95
+ export function encodeEmbeddingBlob(vec) {
96
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
97
+ }
98
+ // ─── Topic-shift detection ───────────────────────────────────────────────────
99
+ /**
100
+ * Detect whether the conversation topic has shifted between the current turn
101
+ * and the previous turn. Uses cosine similarity between topic embeddings.
102
+ *
103
+ * Returns `true` when similarity < TOPIC_SHIFT_THRESHOLD (0.7), meaning the
104
+ * stable cache prefix may need reordering.
105
+ *
106
+ * Returns `false` (no shift) when either embedding is missing.
107
+ */
108
+ export function detectTopicShift(currentEmb, previousEmb) {
109
+ if (!currentEmb || !previousEmb)
110
+ return false;
111
+ return cosineSimilarity(currentEmb, previousEmb) < TOPIC_SHIFT_THRESHOLD;
112
+ }
113
+ // ─── Load embeddings from store ──────────────────────────────────────────────
114
+ /**
115
+ * Load the topic embedding for a given turn from the embedding_cache table.
116
+ * Returns null if no embedding is stored for that turn.
117
+ */
118
+ export function loadTopicEmbedding(stateDir, conversationId, turnIndex) {
119
+ const db = openStore(stateDir);
120
+ try {
121
+ const row = db.prepare(`SELECT embedding FROM embedding_cache WHERE content_hash = ?`).get(`topic:${conversationId}:${turnIndex}`);
122
+ if (!row)
123
+ return null;
124
+ return decodeEmbeddingBlob(row.embedding);
125
+ }
126
+ finally {
127
+ db.close();
128
+ }
129
+ }
130
+ /**
131
+ * Store a topic embedding for a given turn in the embedding_cache table.
132
+ * Idempotent (INSERT OR REPLACE).
133
+ */
134
+ export function storeTopicEmbedding(stateDir, conversationId, turnIndex, embedding) {
135
+ const db = openStore(stateDir);
136
+ try {
137
+ const contentHash = `topic:${conversationId}:${turnIndex}`;
138
+ const blob = encodeEmbeddingBlob(embedding);
139
+ db.prepare(`INSERT OR REPLACE INTO embedding_cache (content_hash, embedding, computed_at) VALUES (?, ?, ?)`).run(contentHash, blob, Date.now());
140
+ }
141
+ finally {
142
+ db.close();
143
+ }
144
+ }
145
+ // ─── Cache stripe management ─────────────────────────────────────────────────
146
+ /**
147
+ * Refresh cache stripe assignments from the cache_stripes table for a given
148
+ * conversation/epoch. Returns rows ordered by stability DESC.
149
+ *
150
+ * This is called at the start of each turn (or on topic-shift) to ensure the
151
+ * stripe assignments reflect the latest stability scores.
152
+ */
153
+ export function refreshStripeAssignments(stateDir, epochId, limit = DEFAULT_STRIPE_LIMIT) {
154
+ const db = openStore(stateDir);
155
+ try {
156
+ const rows = db.prepare(`SELECT chunk_id, stripe, stability, assigned_at, epoch_id
157
+ FROM cache_stripes
158
+ WHERE epoch_id = ?
159
+ ORDER BY stability DESC
160
+ LIMIT ?`).all(epochId, limit);
161
+ return rows;
162
+ }
163
+ finally {
164
+ db.close();
165
+ }
166
+ }
167
+ // ─── Build cache-optimized prompt (Phase 3) ──────────────────────────────────
168
+ /**
169
+ * Build a cache-optimized prompt with 5 layers:
170
+ * 0 (system) -> 1 (summary) -> 2 (cache stripes) -> 3 (thread) -> 4 (tool)
171
+ *
172
+ * Feature gate: MEGACOMPACT_CACHE_STRIPING (default OFF). When OFF, delegates
173
+ * to buildSeparatedPrompt (byte-identical to Phase 2 behavior).
174
+ *
175
+ * When ON but MEGACOMPACT_MESSAGE_SEPARATION is OFF, also delegates to
176
+ * buildSeparatedPrompt (which returns messages unchanged).
177
+ */
178
+ export function buildCacheOptimizedPrompt(messages, opts) {
179
+ const flag = process.env.MEGACOMPACT_CACHE_STRIPING;
180
+ if (flag === "0" || flag === "false" || flag === undefined || flag === "") {
181
+ // Flag OFF: delegate to buildSeparatedPrompt (byte-identical).
182
+ return buildSeparatedPrompt(messages, opts);
183
+ }
184
+ // Build the base 4-layer structure first.
185
+ const base = buildSeparatedPrompt(messages, opts);
186
+ // If base equals messages, separation is OFF — return unchanged.
187
+ if (base === messages)
188
+ return base;
189
+ const stateDir = opts?.stateDir ?? getStateDir();
190
+ // Layer 1 = leading branch/compaction summaries (the stable summary layer).
191
+ // There is no "system" role in pi's AgentMessage union, so Layer 0 is empty.
192
+ // Count leading summaries to find where to insert the cache-stripe layer.
193
+ let layer1End = 0;
194
+ for (let i = 0; i < base.length; i++) {
195
+ const r = base[i].role;
196
+ if (r === "branchSummary" || r === "compactionSummary") {
197
+ layer1End = i + 1;
198
+ }
199
+ else {
200
+ break;
201
+ }
202
+ }
203
+ // Lay out the result: Layer 1 (summaries) first.
204
+ const result = base.slice(0, layer1End);
205
+ // Insert Layer 2: cache stripes (stable context ordered by stability DESC).
206
+ // Try to find the most recent epoch.
207
+ const db = openStore(stateDir);
208
+ let epochId = null;
209
+ try {
210
+ const epoch = db.prepare(`SELECT epoch_id FROM checkpoint_epochs ORDER BY created_at DESC LIMIT 1`).get();
211
+ epochId = epoch?.epoch_id ?? null;
212
+ }
213
+ finally {
214
+ db.close();
215
+ }
216
+ if (epochId) {
217
+ const stripes = refreshStripeAssignments(stateDir, epochId);
218
+ if (stripes.length > 0) {
219
+ // Build a single user message with concatenated stripe content.
220
+ const stripeBlocks = [];
221
+ for (const s of stripes) {
222
+ // Load chunk content from context_chunks table.
223
+ const db2 = openStore(stateDir);
224
+ try {
225
+ const chunk = db2.prepare(`SELECT normalized_text FROM context_chunks WHERE id = ?`).get(s.chunk_id);
226
+ if (chunk?.normalized_text) {
227
+ stripeBlocks.push(chunk.normalized_text);
228
+ }
229
+ }
230
+ finally {
231
+ db2.close();
232
+ }
233
+ }
234
+ if (stripeBlocks.length > 0) {
235
+ result.push({
236
+ role: "user",
237
+ content: stripeBlocks.join("\n\n---\n\n"),
238
+ timestamp: Date.now(),
239
+ });
240
+ }
241
+ }
242
+ }
243
+ // Append remaining layers (3: thread, 4: tool results).
244
+ for (let i = layer1End; i < base.length; i++) {
245
+ result.push(base[i]);
246
+ }
247
+ return result;
248
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * mega-runtime/env-loader.ts — load `.mega-compact.env` from the state dir.
3
+ *
4
+ * The /mega-setup wizard and the dashboard Setup tab write a `.mega-compact.env`
5
+ * file with the user's chosen embedder configuration. Without a loader, that
6
+ * file is write-only dead weight — config is read from process.env at startup
7
+ * (mega-config.ts:loadConfig), so the file was never applied.
8
+ *
9
+ * This loader runs BEFORE loadConfig in the extension entry. It parses the
10
+ * file (KEY=VALUE lines, # comments, quoted values) and applies each key to
11
+ * process.env ONLY if it is not already set — so shell profile / inline env
12
+ * vars always win over the file (the user can override without editing it).
13
+ *
14
+ * Non-fatal: a missing or malformed file never breaks the extension.
15
+ *
16
+ * PREVENT-PI-004: reads a local file only — no network.
17
+ * PREVENT-001: no JSON here; line parser is null/empty-safe.
18
+ */
19
+ import { readFileSync, existsSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ /**
22
+ * Load `.mega-compact.env` from `stateDir` into `process.env`.
23
+ * Keys already set in the environment win (file does not override shell).
24
+ */
25
+ export function loadMegaEnv(stateDir) {
26
+ const envPath = join(stateDir, ".mega-compact.env");
27
+ if (!existsSync(envPath))
28
+ return;
29
+ let content;
30
+ try {
31
+ content = readFileSync(envPath, "utf-8");
32
+ }
33
+ catch {
34
+ return; // unreadable — non-fatal
35
+ }
36
+ for (const line of content.split("\n")) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith("#"))
39
+ continue;
40
+ const eq = trimmed.indexOf("=");
41
+ if (eq < 0)
42
+ continue;
43
+ const key = trimmed.slice(0, eq).trim();
44
+ if (!key)
45
+ continue;
46
+ let val = trimmed.slice(eq + 1).trim();
47
+ if ((val.startsWith('"') && val.endsWith('"')) ||
48
+ (val.startsWith("'") && val.endsWith("'"))) {
49
+ val = val.slice(1, -1);
50
+ }
51
+ // Shell / inline env vars win over the file.
52
+ if (process.env[key] === undefined) {
53
+ process.env[key] = val;
54
+ }
55
+ }
56
+ }
@@ -27,6 +27,7 @@ export function resetRuntimeImpl(self, sessionId) {
27
27
  lastRecallAt: null,
28
28
  lastInjectAt: null,
29
29
  _prevCacheHitPct: null,
30
+ _lastCacheHealthScore: undefined,
30
31
  lastNativeCompactAt: null,
31
32
  compactCount: 0,
32
33
  recallInjections: 0,
@@ -176,6 +176,7 @@ export function snapshotImpl(self, ctx) {
176
176
  armed,
177
177
  modelSnap,
178
178
  providerCachePct,
179
+ perTurnCacheHitPct: self.rt._prevCacheHitPct ?? undefined,
179
180
  });
180
181
  self.widgetData = _snapResult.widgetData;
181
182
  // S33: consume the flare after copying it into widgetData so it fires
@@ -52,6 +52,7 @@ export class MegaRuntime {
52
52
  lastRecallAt: null,
53
53
  lastInjectAt: null,
54
54
  _prevCacheHitPct: null,
55
+ _lastCacheHealthScore: undefined,
55
56
  lastNativeCompactAt: null,
56
57
  compactCount: 0,
57
58
  recallInjections: 0,
@@ -131,6 +131,7 @@ export function computeMegaSnapshot(p) {
131
131
  cachePct,
132
132
  megaCacheFlare: p.megaCacheFlare,
133
133
  megaCacheFlarePct: p.megaCacheFlarePct,
134
+ perTurnCacheHitPct: p.perTurnCacheHitPct ?? cachePct,
134
135
  levelUpFlare: p.levelUpFlare,
135
136
  achievementFlare: p.achievementFlare,
136
137
  achievementFlareTitles: p.achievementFlareTitles,
@@ -87,6 +87,11 @@ export function buildWidgetLines(wd, width, activeAgents) {
87
87
  `${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
88
88
  `${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
89
89
  `${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
90
+ ...(wd.perTurnCacheHitPct != null
91
+ ? [
92
+ `${C.gray}cache hit${C.reset} this turn ${C.green}${Math.round(wd.perTurnCacheHitPct)}%${C.reset}`,
93
+ ]
94
+ : []),
90
95
  ].join(sep);
91
96
  // Wrap to terminal width and pad each line
92
97
  const wrapped = wrapLine(content, width - 2, panelBg); // 2-char indent
@@ -0,0 +1,288 @@
1
+ /**
2
+ * cache-stripe-impl.ts — Vector-Aware Cache Striping implementation.
3
+ *
4
+ * Computes a stability score for each context chunk and assigns it to a
5
+ * cache stripe / prompt-cache layer. Stable chunks (high recency + frequency +
6
+ * semantic density) go to early layers (cached prefix); volatile chunks append
7
+ * at Layer 4 (tail). Runs entirely offline — no network, no LLM (PREVENT-PI-004).
8
+ *
9
+ * The stability score is a weighted composite:
10
+ * stability = 0.5 * semanticSimilarity + 0.3 * recency + 0.2 * frequency
11
+ *
12
+ * - semanticSimilarity: cosine similarity of the chunk's embedding against the
13
+ * running session embedding (from TrigramEmbedder). High similarity means the
14
+ * chunk is topically relevant to current work.
15
+ * - recency: how recently the chunk appeared (normalized to 0.0-1.0 across all
16
+ * chunks in the epoch). Recent chunks are more likely to benefit from caching.
17
+ * - frequency: how often the chunk's content has been referenced (0.0-1.0,
18
+ * estimated from a simple access counter stored alongside).
19
+ *
20
+ * Reassignment happens at epoch boundaries via refreshStripeAssignments.
21
+ * All SQL is parameterized (PREVENT-002). No pi runtime types are imported,
22
+ * keeping this module pi-agnostic.
23
+ */
24
+ import { randomBytes } from "node:crypto";
25
+ import { openStore, withTx } from "./store/sqlite/utils.js";
26
+ // ─── Constants ───────────────────────────────────────────────────────────────
27
+ /** Semantic similarity weight in the composite score. */
28
+ const WEIGHT_SEMANTIC = 0.5;
29
+ /** Recency weight in the composite score. */
30
+ const WEIGHT_RECENCY = 0.3;
31
+ /** Frequency weight in the composite score. */
32
+ const WEIGHT_FREQUENCY = 0.2;
33
+ /** Stripes a chunk lands in based on its stability score. Thresholds define
34
+ * the boundary between adjacent layers. */
35
+ const STRIPE_THRESHOLDS = [
36
+ { minStability: 0.90, stripe: 0 },
37
+ { minStability: 0.70, stripe: 1 },
38
+ { minStability: 0.50, stripe: 2 },
39
+ { minStability: 0.30, stripe: 3 },
40
+ { minStability: -Infinity, stripe: 4 },
41
+ ];
42
+ // ─── Embedding helpers (no external dep) ─────────────────────────────────────
43
+ /**
44
+ * FNV-1a 32-bit hash for the content-based embedding fallback. The production
45
+ * path uses TrigramEmbedder from embedder.ts but we keep a self-contained hash
46
+ * for the case where no embedder is passed in.
47
+ */
48
+ function fnv1a(text) {
49
+ let hash = 0x811c9dc5;
50
+ for (let i = 0; i < text.length; i++) {
51
+ hash ^= text.charCodeAt(i);
52
+ hash = Math.imul(hash, 0x01000193);
53
+ }
54
+ return (hash >>> 0) / 0x100000000;
55
+ }
56
+ /**
57
+ * Produce a crude 128-dim pseudorandom embedding from text using hashed n-gram
58
+ * bins. Matches the approach in TrigramEmbedder._embedRaw conceptually. Used
59
+ * only as a fallback / test path; the caller should prefer TrigramEmbedder.
60
+ */
61
+ function fallbackEmbed(text) {
62
+ const dim = 128;
63
+ const vec = new Array(dim).fill(0);
64
+ const norm = text.toLowerCase().replace(/\s+/g, " ");
65
+ if (norm.length === 0)
66
+ return vec;
67
+ vec[Math.floor(fnv1a(norm) * dim)] += 1;
68
+ for (const word of norm.split(" ")) {
69
+ if (word.length === 0)
70
+ continue;
71
+ vec[Math.floor(fnv1a(word) * dim)] += 0.5;
72
+ for (let i = 0; i < Math.max(1, word.length - 1); i++) {
73
+ const trigram = word.slice(i, i + 3);
74
+ if (trigram.length === 3) {
75
+ vec[Math.floor(fnv1a(trigram) * dim)] += 0.25;
76
+ }
77
+ }
78
+ }
79
+ return l2Normalize(vec);
80
+ }
81
+ function l2Normalize(v) {
82
+ let sumSq = 0;
83
+ for (let i = 0; i < v.length; i++)
84
+ sumSq += v[i] * v[i];
85
+ if (sumSq === 0)
86
+ return v;
87
+ const norm = Math.sqrt(sumSq);
88
+ for (let i = 0; i < v.length; i++)
89
+ v[i] /= norm;
90
+ return v;
91
+ }
92
+ /** Compute cosine similarity between two vectors of equal length. */
93
+ function cosineSimilarity(a, b) {
94
+ if (a.length !== b.length || a.length === 0)
95
+ return 0;
96
+ let dot = 0;
97
+ let na = 0;
98
+ let nb = 0;
99
+ for (let i = 0; i < a.length; i++) {
100
+ dot += a[i] * b[i];
101
+ na += a[i] * a[i];
102
+ nb += b[i] * b[i];
103
+ }
104
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
105
+ return denom === 0 ? 0 : dot / denom;
106
+ }
107
+ // ─── Stability Scoring ───────────────────────────────────────────────────────
108
+ /**
109
+ * Compute a composite stability score for a single chunk.
110
+ *
111
+ * @param chunk The chunk metadata + content to score.
112
+ * @param allChunks All chunks in this epoch (used to compute relative recency).
113
+ * @param embedder Optional embedder instance. If omitted, uses the
114
+ * self-contained fallback (128-dim hashed n-gram).
115
+ * @param sessionEmbed Pre-computed embedding for the current session (the
116
+ * "query" vector). If omitted, computed on the fly from
117
+ * the chunk content alone, which degrades semantic scoring
118
+ * to a self-similarity baseline.
119
+ * @returns A number in [0.0, 1.0] where 1.0 = most stable.
120
+ */
121
+ export function computeStabilityScore(chunk, allChunks, embedder, sessionEmbed) {
122
+ // ── Semantic similarity (0.5 weight) ────────────────────────────────────
123
+ const emb = embedder
124
+ ? embedder.embed(chunk.content)
125
+ : fallbackEmbed(chunk.content);
126
+ // If no session embedding is provided, use the chunk's own embedding as
127
+ // a self-similarity — this produces a baseline score based on content
128
+ // density (chunks with more meaningful content get higher internal
129
+ // similarity). Real deployments should pass the session embedding.
130
+ const sem = cosineSimilarity(emb, sessionEmbed ?? emb);
131
+ const semanticScore = isNaN(sem) ? 0 : sem;
132
+ // ── Recency (0.3 weight) ────────────────────────────────────────────────
133
+ // Relative recency: lastAccessedAt of this chunk vs. min/max across epoch.
134
+ // Falls back to 0.5 if there's only one chunk or no timestamp data.
135
+ let recencyScore = 0.5;
136
+ const accessed = allChunks
137
+ .map((c) => c.lastAccessedAt)
138
+ .filter((t) => t > 0);
139
+ if (accessed.length > 1) {
140
+ const minT = Math.min(...accessed);
141
+ const maxT = Math.max(...accessed);
142
+ const range = maxT - minT;
143
+ if (range > 0) {
144
+ recencyScore = (chunk.lastAccessedAt - minT) / range;
145
+ }
146
+ else {
147
+ recencyScore = 1.0;
148
+ }
149
+ }
150
+ // ── Frequency (0.2 weight) ──────────────────────────────────────────────
151
+ // Access count relative to the max across the epoch.
152
+ const counts = allChunks.map((c) => c.accessCount);
153
+ const maxCount = Math.max(...counts, 1);
154
+ const freqScore = maxCount > 0 ? chunk.accessCount / maxCount : 0;
155
+ // ── Composite ───────────────────────────────────────────────────────────
156
+ const stability = WEIGHT_SEMANTIC * semanticScore +
157
+ WEIGHT_RECENCY * recencyScore +
158
+ WEIGHT_FREQUENCY * freqScore;
159
+ // Clamp to [0.0, 1.0] as a safety net.
160
+ return Math.max(0, Math.min(1, stability));
161
+ }
162
+ /**
163
+ * Determine the cache stripe (layer) for a given stability score.
164
+ *
165
+ * @param stability Composite stability score in [0.0, 1.0].
166
+ * @returns Stripe number 0-4.
167
+ */
168
+ export function stabilityToStripe(stability) {
169
+ for (const t of STRIPE_THRESHOLDS) {
170
+ if (stability >= t.minStability)
171
+ return t.stripe;
172
+ }
173
+ return 4;
174
+ }
175
+ // ─── Stripe Reassignment ─────────────────────────────────────────────────────
176
+ /**
177
+ * Refresh stripe assignments for all chunks in the current epoch.
178
+ *
179
+ * Steps:
180
+ * 1. Read all context_chunks from the SQLite store that belong to the
181
+ * current epoch (or all chunks if no epoch filter).
182
+ * 2. For each chunk, compute a stability score via computeStabilityScore.
183
+ * 3. Map stability -> stripe via stabilityToStripe.
184
+ * 4. UPSERT into cache_stripes.
185
+ * 5. (Stale entries for this epoch are implicitly overwritten by the UPSERT.)
186
+ *
187
+ * The optional embedder parameter allows injecting the production
188
+ * TrigramEmbedder. If omitted, the fallback hashed n-gram embedder is used
189
+ * (works offline in all scenarios).
190
+ *
191
+ * Non-fatal: failures are logged via a provided logger callback and never
192
+ * thrown. Returns the count of chunks reassigned.
193
+ *
194
+ * @param store An open SQLite DatabaseSync handle (or a stateDir string
195
+ * to open lazily). Accepts either to match the caller's
196
+ * convenience. When a string is passed, opens the store for
197
+ * this call only (does not cache the connection).
198
+ * @param epochId The epoch to reassign. If omitted, generates a new epoch
199
+ * ID (random hex). Pass '' to reassign all chunks without
200
+ * filtering by epoch.
201
+ * @param embedder Optional TrigramEmbedder instance. When provided, uses it
202
+ * for semantic similarity; otherwise uses the fallback.
203
+ * @param logFn Optional logging callback (defaults to no-op).
204
+ * @returns The number of chunks that were reassigned.
205
+ */
206
+ export function refreshStripeAssignments(store, epochId, embedder, logFn) {
207
+ const db = typeof store === "string" ? openStore(store) : store;
208
+ const log = logFn ?? (() => { });
209
+ const actualEpochId = epochId ?? nextEpochId();
210
+ const now = Math.floor(Date.now() / 1000);
211
+ try {
212
+ // 1. Read all relevant context_chunks, using the summary as text content.
213
+ // The summary field holds the compressed checkpoint content; for fresh
214
+ // chunks that have no summary yet, fall back to normalized_text or
215
+ // concatenated key_decisions.
216
+ const rows = db
217
+ .prepare(`SELECT c.rowid AS chunk_id,
218
+ COALESCE(c.summary, c.normalized_text, c.key_decisions, '') AS content,
219
+ COALESCE(s.access_count, 0) AS access_count,
220
+ COALESCE(s.last_accessed_at, 0) AS last_accessed_at
221
+ FROM context_chunks c
222
+ LEFT JOIN cache_stripes s ON s.chunk_id = CAST(c.rowid AS TEXT)
223
+ WHERE (? = '' OR s.epoch_id = ? OR s.epoch_id IS NULL)`)
224
+ .all(actualEpochId, actualEpochId);
225
+ if (rows.length === 0) {
226
+ log("cache-stripe: no chunks to reassign");
227
+ return 0;
228
+ }
229
+ // Build the allChunks array for relative scoring.
230
+ const allChunks = rows.map((r) => ({
231
+ chunkId: String(r.chunk_id),
232
+ content: r.content,
233
+ accessCount: r.access_count,
234
+ lastAccessedAt: r.last_accessed_at,
235
+ }));
236
+ // Compute a session embedding (mean of all chunk embeddings) for semantic
237
+ // similarity comparison.
238
+ let sessionEmbed;
239
+ try {
240
+ const dim = embedder ? embedder.embed("").length : 128;
241
+ const sumEmb = new Array(dim).fill(0);
242
+ let count = 0;
243
+ for (const chunk of allChunks) {
244
+ const vec = embedder
245
+ ? embedder.embed(chunk.content)
246
+ : fallbackEmbed(chunk.content);
247
+ for (let i = 0; i < sumEmb.length; i++)
248
+ sumEmb[i] += vec[i];
249
+ count++;
250
+ }
251
+ if (count > 0) {
252
+ for (let i = 0; i < sumEmb.length; i++)
253
+ sumEmb[i] /= count;
254
+ sessionEmbed = l2Normalize(sumEmb);
255
+ }
256
+ }
257
+ catch {
258
+ log("cache-stripe: session embedding failed, skipping semantic weight");
259
+ }
260
+ // 2. Compute stability for each chunk.
261
+ const results = [];
262
+ for (const chunk of allChunks) {
263
+ const stability = computeStabilityScore(chunk, allChunks, embedder, sessionEmbed);
264
+ const stripe = stabilityToStripe(stability);
265
+ results.push({ chunkId: chunk.chunkId, stripe, stability });
266
+ }
267
+ // 3. UPSERT into cache_stripes using a savepoint for atomicity.
268
+ const upsert = db.prepare(`INSERT OR REPLACE INTO cache_stripes(chunk_id, stripe, stability, assigned_at, epoch_id)
269
+ VALUES (?, ?, ?, ?, ?)`);
270
+ withTx(db, () => {
271
+ for (const r of results) {
272
+ upsert.run(r.chunkId, r.stripe, r.stability, now, actualEpochId);
273
+ }
274
+ });
275
+ log(`cache-stripe: reassigned ${results.length} chunks to epoch ${actualEpochId}`);
276
+ return results.length;
277
+ }
278
+ catch (err) {
279
+ log(`cache-stripe: refresh failed: ${err instanceof Error ? err.message : String(err)}`);
280
+ return 0;
281
+ }
282
+ }
283
+ /**
284
+ * Generate a random epoch ID (16 hex chars) for tokenizing stripe cohorts.
285
+ */
286
+ function nextEpochId() {
287
+ return randomBytes(8).toString("hex");
288
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * cache-stripe.ts — Vector-Aware Cache Striping shell (PLAN_V2 Phase 3).
3
+ *
4
+ * Shell: re-exports types and function signatures from the implementation.
5
+ * The heavy logic lives in cache-stripe-impl.ts. This keeps the public API
6
+ * surface clean and respects the src/ 300-line soft limit.
7
+ *
8
+ * Pi-agnostic: no pi runtime types, no network (PREVENT-PI-004).
9
+ * All SQL is parameterized (PREVENT-002).
10
+ */
11
+ export { computeStabilityScore, stabilityToStripe, refreshStripeAssignments, } from "./cache-stripe-impl.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * config/stopwords.ts — shared stopword set for query expansion, coverage
3
+ * metrics, and term tokenization (S45, PREREQUISITE P2).
4
+ *
5
+ * Single source of truth: both `src/queryExpansion.ts` and `src/recallMetrics.ts`
6
+ * import from here instead of duplicating inline sets.
7
+ *
8
+ * Pi-agnostic, zero deps, tree-shake safe.
9
+ */
10
+ /** Default English stopwords. */
11
+ export const STOP_WORDS = new Set([
12
+ "a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
13
+ "of", "with", "by", "from", "as", "is", "was", "are", "were", "be",
14
+ "been", "has", "have", "had", "do", "does", "did", "will", "would",
15
+ "could", "should", "may", "might", "shall", "can", "not", "no", "nor",
16
+ "so", "if", "then", "than", "that", "this", "these", "those", "it",
17
+ "its", "i", "me", "my", "we", "our", "you", "your", "he", "she",
18
+ "they", "them", "their", "what", "which", "who", "whom", "when",
19
+ "where", "why", "how", "all", "each", "every", "both", "few", "more",
20
+ "most", "other", "some", "such", "only", "own", "same", "so", "than",
21
+ "too", "very", "just", "about", "above", "after", "again", "against",
22
+ "because", "before", "between", "down", "during", "out", "over",
23
+ "through", "under", "up", "also", "into", "off", "onto", "upon",
24
+ ]);