pi-mega-compact 0.11.5 → 0.11.6
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/dist/extensions/dashboard-server/api-contracts/cache-stripes.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +13 -0
- package/dist/extensions/dashboard-server/api-contracts/memory-map.js +1 -0
- package/dist/extensions/dashboard-server/routes-cache.js +28 -0
- package/dist/extensions/dashboard-server/routes-game.js +3 -0
- package/dist/extensions/dashboard-server/routes-memory-map.js +50 -0
- package/dist/extensions/dashboard-server/routes-setup.js +105 -0
- package/dist/extensions/dashboard-server/routes.js +3 -1
- package/dist/extensions/dashboard-server/server.js +7 -1
- package/dist/extensions/mega-compact.js +5 -0
- package/dist/extensions/mega-config.js +5 -0
- package/dist/extensions/mega-events/context-handler.js +10 -2
- package/dist/extensions/mega-events/perf-handler.js +83 -1
- package/dist/extensions/mega-events/separated-prompt.js +248 -0
- package/dist/extensions/mega-runtime/env-loader.js +56 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
- package/dist/extensions/mega-runtime/runtime.js +1 -0
- package/dist/extensions/mega-runtime/snapshot.js +1 -0
- package/dist/extensions/mega-runtime/widget.js +5 -0
- package/dist/src/cache-stripe-impl.js +288 -0
- package/dist/src/cache-stripe.js +11 -0
- package/dist/src/config/stopwords.js +24 -0
- package/dist/src/config.js +17 -0
- package/dist/src/memoryGraph.js +299 -0
- package/dist/src/monitoring.js +13 -0
- package/dist/src/queryExpansion.js +163 -0
- package/dist/src/queryReformulation/cache.js +123 -0
- package/dist/src/queryReformulation/rrf.js +29 -0
- package/dist/src/queryReformulation/tfidf.js +61 -0
- package/dist/src/queryReformulation/vagueness.js +33 -0
- package/dist/src/queryReformulation.js +225 -0
- package/dist/src/recallMetrics.js +258 -0
- package/dist/src/store/sqlite/cache-stripes.js +133 -0
- package/dist/src/store/sqlite/fts5-search.js +68 -0
- package/dist/src/store/sqlite/perf-samples.js +1 -0
- package/dist/src/store/sqlite/schema.js +60 -1
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store/vectorIndex.js +29 -5
- package/dist/src/tiered-router/types.js +18 -0
- package/dist/src/tieredRouter.js +421 -0
- package/dist/src/vectorStore.js +8 -6
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js → AchievementTiles-DR9cLGDL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js.map → AchievementTiles-DR9cLGDL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js → AchievementsTab-svNUgJ1e.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js.map → AchievementsTab-svNUgJ1e.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js → ConfigTab-CmmWheuW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js.map → ConfigTab-CmmWheuW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js → EventsTab-CFE7jLsa.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js.map → EventsTab-CFE7jLsa.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/GameTab-CJAX_th_.js +2 -0
- package/extensions/dashboard-client/dist/assets/{GameTab-DehUX-KE.js.map → GameTab-CJAX_th_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js → MaintenanceTab-P4SUsvMf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js.map → MaintenanceTab-P4SUsvMf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js → MetricsTab-CHQscixy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js.map → MetricsTab-CHQscixy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js → OverviewTab-DBs4DJdd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js.map → OverviewTab-DBs4DJdd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js → ReposTab-DeFz76L5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js.map → ReposTab-DeFz76L5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js → SessionsTab-BWNsj1OB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js.map → SessionsTab-BWNsj1OB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js → TimeSavedCard-CiqCOQuL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js.map → TimeSavedCard-CiqCOQuL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js → TopicsTab-BWWQxltv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js.map → TopicsTab-BWWQxltv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js → TurnsTab-CVz4f4-o.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js.map → TurnsTab-CVz4f4-o.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-4gQ7auad.js → index-DrktNOoT.js} +11 -11
- package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js → useSSE-Bf5LOLSi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js.map → useSSE-Bf5LOLSi.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +24 -8
- package/extensions/dashboard-client/src/api/client.ts +15 -0
- package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +100 -0
- package/extensions/dashboard-client/src/components/StripeDistributionCard.tsx +110 -0
- package/extensions/dashboard-client/src/components/TabBar.tsx +49 -4
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/base.css +30 -0
- package/extensions/dashboard-client/src/styles/cache.css +167 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +75 -13
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +440 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +65 -2
- package/extensions/dashboard-server/api-contracts/cache-stripes.ts +49 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -1
- package/extensions/dashboard-server/api-contracts/index.ts +16 -1
- package/extensions/dashboard-server/api-contracts/memory-map.ts +50 -0
- package/extensions/dashboard-server/api-contracts/setup.ts +22 -0
- package/extensions/dashboard-server/routes-cache.ts +36 -1
- package/extensions/dashboard-server/routes-game.ts +3 -0
- package/extensions/dashboard-server/routes-memory-map.ts +105 -0
- package/extensions/dashboard-server/routes-setup.ts +112 -0
- package/extensions/dashboard-server/routes.ts +3 -1
- package/extensions/dashboard-server/server.ts +6 -0
- package/extensions/mega-compact.ts +5 -0
- package/extensions/mega-config.ts +17 -0
- package/extensions/mega-events/context-handler.ts +10 -2
- package/extensions/mega-events/perf-handler.ts +111 -1
- package/extensions/mega-events/separated-prompt.ts +329 -0
- package/extensions/mega-runtime/env-loader.ts +55 -0
- package/extensions/mega-runtime/helpers.ts +2 -0
- package/extensions/mega-runtime/reset-runtime.ts +1 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +1 -0
- package/extensions/mega-runtime/runtime.ts +1 -0
- package/extensions/mega-runtime/snapshot.ts +6 -0
- package/extensions/mega-runtime/widget-types.ts +3 -0
- package/extensions/mega-runtime/widget.ts +5 -0
- package/package.json +1 -1
- package/src/cache-stripe-impl.ts +385 -0
- package/src/cache-stripe.ts +18 -0
- package/src/config/stopwords.ts +25 -0
- package/src/config.ts +26 -0
- package/src/memoryGraph.ts +438 -0
- package/src/monitoring.ts +35 -0
- package/src/queryExpansion.ts +206 -0
- package/src/queryReformulation/cache.ts +169 -0
- package/src/queryReformulation/rrf.ts +51 -0
- package/src/queryReformulation/tfidf.ts +79 -0
- package/src/queryReformulation/vagueness.ts +38 -0
- package/src/queryReformulation.ts +355 -0
- package/src/recallMetrics.ts +388 -0
- package/src/store/sqlite/cache-stripes.ts +196 -0
- package/src/store/sqlite/fts5-search.ts +90 -0
- package/src/store/sqlite/perf-samples.ts +3 -1
- package/src/store/sqlite/schema.ts +60 -1
- package/src/store/sqlite.ts +1 -0
- package/src/store/vectorIndex.ts +38 -7
- package/src/tiered-router/types.ts +64 -0
- package/src/tieredRouter.ts +489 -0
- package/src/vectorStore.ts +9 -5
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/GameTab-DehUX-KE.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +0 -1
- 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
|
+
}
|
|
@@ -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
|
|
@@ -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
|
+
]);
|