pi-mega-compact 0.13.4 → 0.13.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/README.md +11 -8
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +50 -0
- package/dist/extensions/dashboard-server/api-contracts/rag-metrics.js +1 -0
- package/dist/extensions/dashboard-server/api-contracts/rag-settings.js +3 -3
- package/dist/extensions/dashboard-server/api-contracts/wiki.js +1 -0
- package/dist/extensions/dashboard-server/routes-rag-metrics.js +60 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +167 -0
- package/dist/extensions/dashboard-server/routes-rag-settings.js +136 -77
- package/dist/extensions/dashboard-server/routes-wiki-helpers.js +97 -0
- package/dist/extensions/dashboard-server/routes-wiki.js +330 -0
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +17 -3
- package/dist/extensions/mega-events/context-handler/afterCompact.js +35 -2
- package/dist/extensions/mega-pipeline/recall.js +31 -0
- package/dist/extensions/mega-turn-store.js +2 -1
- package/dist/src/config/turns.js +10 -0
- package/dist/src/config.js +2 -0
- package/dist/src/recall/async.js +10 -1
- package/dist/src/recall/format.js +10 -1
- package/dist/src/recall/hydeTelemetry.js +22 -0
- package/dist/src/recall/sync.js +28 -2
- package/dist/src/store/turns/hydeStore.js +32 -0
- package/dist/src/store/turns/index.js +1 -0
- package/dist/src/store/turns/schema.js +102 -1
- package/dist/src/store/turns/sqlite-store/rows.js +23 -0
- package/dist/src/store/turns/sqlite-store/write.js +7 -3
- package/dist/src/topics/durability.js +187 -0
- package/dist/src/topics/index.js +2 -1
- package/dist/src/topics/store.js +28 -9
- package/dist/src/wiki/curation-helpers.js +71 -0
- package/dist/src/wiki/curation.js +147 -0
- package/dist/src/wiki/incremental.js +126 -0
- package/dist/src/wiki/index.js +5 -0
- package/extensions/dashboard-client/dist/assets/AreaChart-KAmp_6bL.js +70 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C30AwyoF.js.map → AreaChart-KAmp_6bL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Bk-gm-R0.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Bk-gm-R0.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-Cyylo0of.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-Cyylo0of.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-D-IzmV29.js +2 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-D-IzmV29.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-CrlkCFVj.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-CrlkCFVj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DHUdZ7RR.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DHUdZ7RR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-B7I4HWVg.js +2 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-B7I4HWVg.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-K6j4ZI7u.js +2 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-K6j4ZI7u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-BSRWhn56.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-BSRWhn56.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-BXdizHN1.js +2 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-BXdizHN1.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-CAgyfqY3.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-CAgyfqY3.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-DwgCzPD9.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-DwgCzPD9.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CVKcStBp.js +5 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CVKcStBp.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-BNohxTbs.js +2 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-BNohxTbs.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/button-B4A-_dCA.js +2 -0
- package/extensions/dashboard-client/dist/assets/button-B4A-_dCA.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/card-Cjn7Ifoe.js +2 -0
- package/extensions/dashboard-client/dist/assets/card-Cjn7Ifoe.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CQjCLvv7.js +137 -0
- package/extensions/dashboard-client/dist/assets/index-CQjCLvv7.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +1 -0
- package/extensions/dashboard-client/dist/assets/switch-CcdXX0Zm.js +2 -0
- package/extensions/dashboard-client/dist/assets/switch-CcdXX0Zm.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/toggle-KdBnFLu3.js +2 -0
- package/extensions/dashboard-client/dist/assets/toggle-KdBnFLu3.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js → useSSE-B6t44vzc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js.map → useSSE-B6t44vzc.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/package-lock.json +1995 -191
- package/extensions/dashboard-client/package.json +12 -1
- package/extensions/dashboard-client/postcss.config.js +6 -0
- package/extensions/dashboard-client/src/App.tsx +127 -88
- package/extensions/dashboard-client/src/api/client.ts +109 -10
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +15 -7
- package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +67 -55
- package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +17 -13
- package/extensions/dashboard-client/src/components/CacheHitsCard.tsx +35 -30
- package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +33 -28
- package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +5 -6
- package/extensions/dashboard-client/src/components/EventStream.tsx +17 -13
- package/extensions/dashboard-client/src/components/HydeDetailPanel.tsx +173 -0
- package/extensions/dashboard-client/src/components/LegendCard.tsx +16 -9
- package/extensions/dashboard-client/src/components/PerfCards.tsx +28 -20
- package/extensions/dashboard-client/src/components/PerfChart.tsx +9 -6
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +15 -7
- package/extensions/dashboard-client/src/components/RagDashboard.tsx +253 -0
- package/extensions/dashboard-client/src/components/RepoAllSessionsCard.tsx +15 -10
- package/extensions/dashboard-client/src/components/RepoContextStack.tsx +24 -16
- package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +52 -49
- package/extensions/dashboard-client/src/components/RepoTable.tsx +26 -17
- package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +11 -14
- package/extensions/dashboard-client/src/components/SessionInfo.tsx +20 -15
- package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +8 -5
- package/extensions/dashboard-client/src/components/SummaryTiles.tsx +26 -17
- package/extensions/dashboard-client/src/components/TabBar.tsx +1 -3
- package/extensions/dashboard-client/src/components/layout/AppShell.tsx +42 -0
- package/extensions/dashboard-client/src/components/layout/BottomBar.tsx +88 -0
- package/extensions/dashboard-client/src/components/layout/Sidebar.tsx +83 -0
- package/extensions/dashboard-client/src/components/ui/badge.tsx +30 -0
- package/extensions/dashboard-client/src/components/ui/button.tsx +44 -0
- package/extensions/dashboard-client/src/components/ui/card.tsx +32 -0
- package/extensions/dashboard-client/src/components/ui/select.tsx +64 -0
- package/extensions/dashboard-client/src/components/ui/switch.tsx +26 -0
- package/extensions/dashboard-client/src/components/ui/tabs.tsx +36 -0
- package/extensions/dashboard-client/src/components/ui/toggle.tsx +28 -0
- package/extensions/dashboard-client/src/components/ui/tooltip.tsx +27 -0
- package/extensions/dashboard-client/src/config.ts +10 -0
- package/extensions/dashboard-client/src/main.tsx +2 -2
- package/extensions/dashboard-client/src/memory-map-layout.ts +1 -2
- package/extensions/dashboard-client/src/styles/index.css +79 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +33 -26
- package/extensions/dashboard-client/src/tabs/EventsTab.tsx +22 -7
- package/extensions/dashboard-client/src/tabs/HealthTab.tsx +77 -46
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/ActionsCard.tsx +60 -50
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DbStatsCard.tsx +61 -35
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DebugBundleCard.tsx +34 -23
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/HealthMitigationCard.tsx +62 -43
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/SchemaHealthCard.tsx +53 -32
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +2 -2
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/MemoryMapView.tsx +29 -46
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +10 -34
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +14 -34
- package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +5 -2
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +11 -5
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +5 -5
- package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +47 -33
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +6 -2
- package/extensions/dashboard-client/src/tabs/SetupTab/RagHealthCard.tsx +78 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/SettingsPanel.tsx +198 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/SettingsSection.tsx +50 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +21 -59
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +75 -40
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +150 -112
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicEvolutionGraph.tsx +274 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicEvolutionView.tsx +71 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicTimeline.tsx +103 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/WikiPage.tsx +213 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/WikiPageControls.tsx +188 -0
- package/extensions/dashboard-client/src/tabs/WikiTab.tsx +222 -0
- package/extensions/dashboard-client/src/tabs/registry.ts +57 -0
- package/extensions/dashboard-client/src/utils/cn.ts +7 -0
- package/extensions/dashboard-client/tailwind.config.js +46 -0
- package/extensions/dashboard-server/api-contracts/core.ts +59 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +79 -0
- package/extensions/dashboard-server/api-contracts/index.ts +48 -2
- package/extensions/dashboard-server/api-contracts/rag-metrics.ts +22 -0
- package/extensions/dashboard-server/api-contracts/rag-settings.ts +30 -20
- package/extensions/dashboard-server/api-contracts/wiki.ts +138 -0
- package/extensions/dashboard-server/routes-rag-metrics.ts +82 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +314 -0
- package/extensions/dashboard-server/routes-rag-settings.ts +147 -97
- package/extensions/dashboard-server/routes-wiki-helpers.ts +126 -0
- package/extensions/dashboard-server/routes-wiki.ts +376 -0
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +18 -2
- package/extensions/mega-events/context-handler/afterCompact.ts +44 -1
- package/extensions/mega-pipeline/recall.ts +31 -0
- package/extensions/mega-turn-store.ts +13 -0
- package/package.json +1 -1
- package/src/config/turns.ts +17 -0
- package/src/config.ts +3 -0
- package/src/recall/async.ts +10 -1
- package/src/recall/format.ts +15 -2
- package/src/recall/hydeTelemetry.ts +44 -0
- package/src/recall/sync.ts +34 -2
- package/src/recall/types.ts +36 -0
- package/src/store/turns/hydeStore.ts +82 -0
- package/src/store/turns/index.ts +4 -0
- package/src/store/turns/schema.ts +126 -1
- package/src/store/turns/sqlite-store/rows.ts +25 -0
- package/src/store/turns/sqlite-store/write.ts +20 -2
- package/src/store/turns/types.ts +33 -0
- package/src/topics/durability.ts +231 -0
- package/src/topics/index.ts +7 -1
- package/src/topics/store.ts +35 -9
- package/src/wiki/curation-helpers.ts +140 -0
- package/src/wiki/curation.ts +217 -0
- package/src/wiki/incremental.ts +164 -0
- package/src/wiki/index.ts +13 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Cqnc7lMg.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-Cqnc7lMg.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/EventsTab-tYk3VFAp.js +0 -2
- package/extensions/dashboard-client/dist/assets/EventsTab-tYk3VFAp.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/HealthTab-D2_kjgxw.js +0 -2
- package/extensions/dashboard-client/dist/assets/HealthTab-D2_kjgxw.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Bc-uPYrz.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Bc-uPYrz.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-D1V9g_NC.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-D1V9g_NC.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MetricsTab-DT_XfL96.js +0 -2
- package/extensions/dashboard-client/dist/assets/MetricsTab-DT_XfL96.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-BY6KZCme.js +0 -2
- package/extensions/dashboard-client/dist/assets/OverviewTab-BY6KZCme.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-CeS-tpDM.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-CeS-tpDM.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/SessionsTab-C30AwyoF.js +0 -70
- package/extensions/dashboard-client/dist/assets/SetupTab-BJUHOVK2.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-BJUHOVK2.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-p55K1tFP.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-p55K1tFP.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-D8FqyAbK.js +0 -2
- package/extensions/dashboard-client/dist/assets/TopicsTab-D8FqyAbK.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-ju-nXDys.js +0 -2
- package/extensions/dashboard-client/dist/assets/TurnsTab-ju-nXDys.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-BEj1Zahy.js +0 -42
- package/extensions/dashboard-client/dist/assets/index-BEj1Zahy.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +0 -1
- package/extensions/dashboard-client/src/styles/metrics-extra.css +0 -69
- package/extensions/dashboard-client/src/styles/turns.css +0 -142
- package/extensions/dashboard-client/src/tabs/SetupTab/RagSettingsCard.tsx +0 -160
|
@@ -142,8 +142,17 @@ export function scoreAndLogRecallMetrics(query, toInject) {
|
|
|
142
142
|
diversity: metrics.breakdown.diversity,
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
return {
|
|
146
|
+
hitCount: toInject.length,
|
|
147
|
+
score: metrics.score,
|
|
148
|
+
pass: metrics.pass,
|
|
149
|
+
relevance: metrics.breakdown.relevance,
|
|
150
|
+
coverage: metrics.breakdown.coverage,
|
|
151
|
+
diversity: metrics.breakdown.diversity,
|
|
152
|
+
specificity: metrics.breakdown.specificity,
|
|
153
|
+
};
|
|
145
154
|
}
|
|
146
155
|
catch {
|
|
147
|
-
|
|
156
|
+
return null; // non-fatal: metrics never break recall
|
|
148
157
|
}
|
|
149
158
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function buildHydeInfo(outcome, hypotheticalDoc, generationMs, rawHitCount, hydeHitCount, fusedHitCount) {
|
|
2
|
+
const ran = outcome === "ran";
|
|
3
|
+
const skipped = !ran;
|
|
4
|
+
// Lift = fusedHitCount / max(1, rawHitCount) — how much HyDE changed recall
|
|
5
|
+
// breadth. Skipped passes yield 1 (raw hits cascade unchanged) unless there
|
|
6
|
+
// were no raw hits to compare against.
|
|
7
|
+
const lift = fusedHitCount / Math.max(1, rawHitCount);
|
|
8
|
+
return {
|
|
9
|
+
ran,
|
|
10
|
+
skipped,
|
|
11
|
+
reason: outcome,
|
|
12
|
+
hypotheticalDoc,
|
|
13
|
+
generationMs: ran ? generationMs : 0,
|
|
14
|
+
rawHitCount,
|
|
15
|
+
hydeHitCount: ran ? hydeHitCount : 0,
|
|
16
|
+
fusedHitCount,
|
|
17
|
+
lift: Math.round(lift * 100) / 100,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function hydeSkipped(reason, rawHitCount, fusedHitCount) {
|
|
21
|
+
return buildHydeInfo(reason, "", 0, rawHitCount, 0, fusedHitCount);
|
|
22
|
+
}
|
package/dist/src/recall/sync.js
CHANGED
|
@@ -21,6 +21,7 @@ import { RAG_QUERY_REFORMULATION, RAG_TIERED_ROUTER, RAG_RECALL_METRICS, RAG_HYD
|
|
|
21
21
|
import { generateHypotheticalDoc, fuseRecallHits } from "../hyde.js";
|
|
22
22
|
import { reformulateRecallQuery, runTieredRecall } from "./reformulate.js";
|
|
23
23
|
import { formatRecallBlock, scoreAndLogRecallMetrics, raptorOverviewBlock } from "./format.js";
|
|
24
|
+
import { buildHydeInfo, hydeSkipped } from "./hydeTelemetry.js";
|
|
24
25
|
/**
|
|
25
26
|
* Recall and inline context from the checkpoint store.
|
|
26
27
|
*
|
|
@@ -84,10 +85,17 @@ export function recallAndInline(opts, store) {
|
|
|
84
85
|
// RRF-fuses with the raw-query results. Non-fatal: any error → raw-only.
|
|
85
86
|
// TrigramEmbedder (kind !== "http") skips this — S57 B1 reformulation above
|
|
86
87
|
// already handles the no-LLM path.
|
|
88
|
+
// H1 telemetry tracking for the HyDE block below.
|
|
87
89
|
let hydeHits = null;
|
|
90
|
+
let hydeDoc = "";
|
|
91
|
+
let hydeGenMs = 0;
|
|
92
|
+
let hydeRawCount = 0;
|
|
88
93
|
if (RAG_HYDE_ENABLED() && embedder.kind === "http") {
|
|
94
|
+
const t0 = Date.now();
|
|
89
95
|
const hyde = generateHypotheticalDoc(opts.query, embedder);
|
|
96
|
+
hydeGenMs = Date.now() - t0;
|
|
90
97
|
if (hyde) {
|
|
98
|
+
hydeDoc = hyde;
|
|
91
99
|
try {
|
|
92
100
|
const r2 = searchRecall({ sessionId: opts.sessionId, query: hyde, limit, skipInjected: skip }, store);
|
|
93
101
|
hydeHits = r2.newHits;
|
|
@@ -105,6 +113,8 @@ export function recallAndInline(opts, store) {
|
|
|
105
113
|
const result = searchRecall({ sessionId: opts.sessionId, query: recallQuery, limit, skipInjected: skip }, store);
|
|
106
114
|
newHits = result.newHits;
|
|
107
115
|
}
|
|
116
|
+
// H1: the raw-query hit count is taken BEFORE fusion.
|
|
117
|
+
hydeRawCount = newHits.length;
|
|
108
118
|
// S43 re-plan: fuse the hypothetical-doc hits with the raw-query hits via RRF
|
|
109
119
|
// when HyDE produced results. The downstream inline dedupe + metrics + format
|
|
110
120
|
// operate on this fused set.
|
|
@@ -142,8 +152,10 @@ export function recallAndInline(opts, store) {
|
|
|
142
152
|
// byte-identical). Scores the injected hits against the ORIGINAL query so the
|
|
143
153
|
// metric measures whether the (possibly expanded) search results stay
|
|
144
154
|
// relevant to what the user actually asked.
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
let recallMetrics = null;
|
|
156
|
+
if (RAG_RECALL_METRICS()) {
|
|
157
|
+
recallMetrics = scoreAndLogRecallMetrics(opts.query, toInject);
|
|
158
|
+
}
|
|
147
159
|
// F3: format once — one preamble, correct [1..n] numbering.
|
|
148
160
|
const recallBlock = toInject.length > 0 ? formatRecallBlock(toInject) : "";
|
|
149
161
|
const report = toInject.map((h) => ` • ${h.checkpoint.checkpointId} (${h.checkpoint.summary.slice(0, 60).replace(/\n/g, " ")}…)`);
|
|
@@ -161,10 +173,24 @@ export function recallAndInline(opts, store) {
|
|
|
161
173
|
const block = overview && recallBlock
|
|
162
174
|
? overview + "\n" + recallBlock
|
|
163
175
|
: overview || recallBlock;
|
|
176
|
+
// H1: build the HyDE invocation telemetry for this pass.
|
|
177
|
+
const fusedCount = toInject.length;
|
|
178
|
+
let hydeInfo;
|
|
179
|
+
if (RAG_HYDE_ENABLED() && embedder.kind === "http") {
|
|
180
|
+
hydeInfo =
|
|
181
|
+
hydeHits && hydeHits.length > 0
|
|
182
|
+
? buildHydeInfo("ran", hydeDoc, hydeGenMs, hydeRawCount, hydeHits.length, fusedCount)
|
|
183
|
+
: hydeSkipped(hydeDoc ? "generation-failed" : "no-llm", hydeRawCount, fusedCount);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
hydeInfo = hydeSkipped("disabled", hydeRawCount, fusedCount);
|
|
187
|
+
}
|
|
164
188
|
return {
|
|
165
189
|
toInject,
|
|
166
190
|
report,
|
|
167
191
|
block,
|
|
168
192
|
empty: block.length === 0,
|
|
193
|
+
hydeInfo,
|
|
194
|
+
recallMetrics,
|
|
169
195
|
};
|
|
170
196
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function listTelemetryTurns(db, opts = {}) {
|
|
2
|
+
const limit = Math.max(1, opts.limit ?? 200);
|
|
3
|
+
return db
|
|
4
|
+
.prepare(`SELECT id AS turnId, conversation_id AS conversationId,
|
|
5
|
+
turn_index AS turnIndex, role, ended_at AS endedAt,
|
|
6
|
+
hyde_ran AS hydeRan, hyde_reason AS hydeReason, hyde_doc AS hydeDoc,
|
|
7
|
+
hyde_raw_count AS hydeRawCount, hyde_hyde_count AS hydeHydeCount,
|
|
8
|
+
hyde_fused_count AS hydeFusedCount, hyde_lift AS hydeLift,
|
|
9
|
+
hyde_generation_ms AS hydeGenerationMs,
|
|
10
|
+
recall_score AS recallScore, recall_pass AS recallPass,
|
|
11
|
+
recall_relevance AS recallRelevance, recall_coverage AS recallCoverage,
|
|
12
|
+
recall_diversity AS recallDiversity, recall_specificity AS recallSpecificity
|
|
13
|
+
FROM turns
|
|
14
|
+
WHERE hyde_ran = 1 OR recall_pass IS NOT NULL
|
|
15
|
+
ORDER BY ended_at DESC
|
|
16
|
+
LIMIT ?`)
|
|
17
|
+
.all(limit);
|
|
18
|
+
}
|
|
19
|
+
export function aggregateDailyTelemetry(db, days) {
|
|
20
|
+
const since = Date.now() - days * 86_400_000;
|
|
21
|
+
return db
|
|
22
|
+
.prepare(`SELECT date(ended_at / 1000, 'unixepoch') AS day,
|
|
23
|
+
COUNT(*) AS recallCount,
|
|
24
|
+
SUM(hyde_ran) AS hydeRanCount,
|
|
25
|
+
AVG(recall_score) AS avgScore,
|
|
26
|
+
AVG(hyde_lift) AS avgLift,
|
|
27
|
+
AVG(hyde_generation_ms) AS avgGenMs
|
|
28
|
+
FROM turns
|
|
29
|
+
WHERE ended_at >= ? AND (hyde_ran = 1 OR recall_pass IS NOT NULL)
|
|
30
|
+
GROUP BY day ORDER BY day ASC`)
|
|
31
|
+
.all(since);
|
|
32
|
+
}
|
|
@@ -15,6 +15,7 @@ export { InMemoryTurnStore } from "./memory-store.js";
|
|
|
15
15
|
export { openTurnDb, openTurnStore, closeTurnDb, closeTurnStore, closeAllTurnDbs, turnDbPath, TURNS_DB_FILE, withTx, } from "./connection.js";
|
|
16
16
|
export { initTurnSchema } from "./schema.js";
|
|
17
17
|
export { migrateTurnTablesIfNeeded } from "./migrations.js";
|
|
18
|
+
export { listTelemetryTurns, aggregateDailyTelemetry } from "./hydeStore.js";
|
|
18
19
|
import { SqliteTurnStore } from "./sqlite-store.js";
|
|
19
20
|
import { InMemoryTurnStore } from "./memory-store.js";
|
|
20
21
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Schema version stamp (written once into turns_meta). */
|
|
2
|
-
const SCHEMA_VERSION =
|
|
2
|
+
const SCHEMA_VERSION = 3;
|
|
3
3
|
/** Create the unified turn-store schema if absent. Idempotent. */
|
|
4
4
|
export function initTurnSchema(db) {
|
|
5
5
|
// Migration: v1 had a pressure_band CHECK constraint that only allowed
|
|
@@ -121,6 +121,107 @@ export function initTurnSchema(db) {
|
|
|
121
121
|
)
|
|
122
122
|
`);
|
|
123
123
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_topics_topic ON memory_topics(topic_id)`);
|
|
124
|
+
// H1: additive HyDE + recall-quality telemetry columns. ALTER TABLE ADD
|
|
125
|
+
// COLUMN (idempotent via PRAGMA guard) — turns stays append-only; these land
|
|
126
|
+
// in the single INSERT at turn-record time, never via UPDATE.
|
|
127
|
+
const ensureCol = (name, ddl) => {
|
|
128
|
+
const has = db.prepare("PRAGMA table_info(turns)").all().some((c) => c.name === name);
|
|
129
|
+
if (!has)
|
|
130
|
+
db.exec(`ALTER TABLE turns ADD COLUMN ${ddl}`);
|
|
131
|
+
};
|
|
132
|
+
ensureCol("hyde_ran", "hyde_ran INTEGER DEFAULT 0");
|
|
133
|
+
ensureCol("hyde_doc", "hyde_doc TEXT DEFAULT ''");
|
|
134
|
+
ensureCol("hyde_raw_count", "hyde_raw_count INTEGER DEFAULT 0");
|
|
135
|
+
ensureCol("hyde_hyde_count", "hyde_hyde_count INTEGER DEFAULT 0");
|
|
136
|
+
ensureCol("hyde_fused_count", "hyde_fused_count INTEGER DEFAULT 0");
|
|
137
|
+
ensureCol("hyde_lift", "hyde_lift REAL DEFAULT 0");
|
|
138
|
+
ensureCol("hyde_generation_ms", "hyde_generation_ms INTEGER DEFAULT 0");
|
|
139
|
+
ensureCol("hyde_reason", "hyde_reason TEXT DEFAULT ''");
|
|
140
|
+
ensureCol("recall_score", "recall_score REAL DEFAULT 0");
|
|
141
|
+
ensureCol("recall_pass", "recall_pass INTEGER DEFAULT 0");
|
|
142
|
+
ensureCol("recall_relevance", "recall_relevance REAL DEFAULT 0");
|
|
143
|
+
ensureCol("recall_coverage", "recall_coverage REAL DEFAULT 0");
|
|
144
|
+
ensureCol("recall_diversity", "recall_diversity REAL DEFAULT 0");
|
|
145
|
+
ensureCol("recall_specificity", "recall_specificity REAL DEFAULT 0");
|
|
146
|
+
// ── Wiki Revival (Spec 3): user curation + topic evolution ────────
|
|
147
|
+
const sessionCol = db.prepare("PRAGMA table_info(memory_topics)").all().some((c) => c.name === "session_id");
|
|
148
|
+
if (!sessionCol) {
|
|
149
|
+
db.exec("ALTER TABLE memory_topics ADD COLUMN session_id TEXT");
|
|
150
|
+
}
|
|
151
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_topics_session ON memory_topics(session_id)`);
|
|
152
|
+
db.exec(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS topic_overrides (
|
|
154
|
+
topic_id TEXT NOT NULL,
|
|
155
|
+
kind TEXT NOT NULL CHECK(kind IN ('label','merge','split')),
|
|
156
|
+
custom_label TEXT,
|
|
157
|
+
merged_into TEXT,
|
|
158
|
+
split_from TEXT,
|
|
159
|
+
split_memory_ids TEXT,
|
|
160
|
+
merged_memory_ids TEXT,
|
|
161
|
+
overridden_at INTEGER NOT NULL,
|
|
162
|
+
PRIMARY KEY (topic_id, kind)
|
|
163
|
+
)
|
|
164
|
+
`);
|
|
165
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_topic_overrides_topic ON topic_overrides(topic_id)`);
|
|
166
|
+
// W5: add merged_memory_ids column to existing topic_overrides tables (same
|
|
167
|
+
// additive ensureColumn pattern as the HyDE columns). Stores the member ids
|
|
168
|
+
// from the source topic at merge time so merge overrides survive a full
|
|
169
|
+
// replaceTopicModel rebuild (topic_evolution rows are CASCADE-deleted with
|
|
170
|
+
// their parent topic, but topic_overrides rows persist across rebuilds).
|
|
171
|
+
const overridesCols = db
|
|
172
|
+
.prepare("PRAGMA table_info(topic_overrides)")
|
|
173
|
+
.all();
|
|
174
|
+
if (overridesCols.length > 0 &&
|
|
175
|
+
!overridesCols.some((c) => c.name === "merged_memory_ids")) {
|
|
176
|
+
db.exec("ALTER TABLE topic_overrides ADD COLUMN merged_memory_ids TEXT");
|
|
177
|
+
}
|
|
178
|
+
db.exec(`
|
|
179
|
+
CREATE TABLE IF NOT EXISTS topic_evolution (
|
|
180
|
+
topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
|
181
|
+
memory_id TEXT NOT NULL,
|
|
182
|
+
session_id TEXT,
|
|
183
|
+
assigned_at INTEGER NOT NULL,
|
|
184
|
+
method TEXT NOT NULL CHECK(method IN ('kmeans+tfidf','merge','split','manual')),
|
|
185
|
+
PRIMARY KEY (topic_id, memory_id)
|
|
186
|
+
)
|
|
187
|
+
`);
|
|
188
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_topic_evolution_topic ON topic_evolution(topic_id, assigned_at)`);
|
|
189
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_topic_evolution_ts ON topic_evolution(assigned_at)`);
|
|
190
|
+
// Migration: topic_evolution.topic_id FK lacked ON DELETE CASCADE when the
|
|
191
|
+
// table was first created (W1). Without it, deleting a topic (merge source or
|
|
192
|
+
// replaceTopicModel rebuild) throws SQLITE_CONSTRAINT when evolution rows
|
|
193
|
+
// reference it, rolling back the delete. Recreate the table with CASCADE when
|
|
194
|
+
// the live FK definition is missing it. Non-fatal: a failure just leaves the
|
|
195
|
+
// old definition (rebuilds remain best-effort).
|
|
196
|
+
try {
|
|
197
|
+
// PRAGMA foreign_key_list exposes the referenced table + on-delete rule.
|
|
198
|
+
const fkList = db
|
|
199
|
+
.prepare("PRAGMA foreign_key_list(topic_evolution)")
|
|
200
|
+
.all();
|
|
201
|
+
const topicFk = fkList.find((f) => f.from === "topic_id");
|
|
202
|
+
if (topicFk && topicFk.on_delete !== "CASCADE") {
|
|
203
|
+
db.exec("DROP TABLE IF EXISTS topic_evolution_mig");
|
|
204
|
+
db.exec(`
|
|
205
|
+
CREATE TABLE topic_evolution_mig (
|
|
206
|
+
topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
|
207
|
+
memory_id TEXT NOT NULL,
|
|
208
|
+
session_id TEXT,
|
|
209
|
+
assigned_at INTEGER NOT NULL,
|
|
210
|
+
method TEXT NOT NULL CHECK(method IN ('kmeans+tfidf','merge','split','manual')),
|
|
211
|
+
PRIMARY KEY (topic_id, memory_id)
|
|
212
|
+
)
|
|
213
|
+
`);
|
|
214
|
+
db.exec(`INSERT INTO topic_evolution_mig (topic_id, memory_id, session_id, assigned_at, method)
|
|
215
|
+
SELECT topic_id, memory_id, session_id, assigned_at, method FROM topic_evolution`);
|
|
216
|
+
db.exec("DROP TABLE topic_evolution");
|
|
217
|
+
db.exec("ALTER TABLE topic_evolution_mig RENAME TO topic_evolution");
|
|
218
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_topic_evolution_topic ON topic_evolution(topic_id, assigned_at)`);
|
|
219
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_topic_evolution_ts ON topic_evolution(assigned_at)`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
/* non-fatal: migration never breaks turn-DB open */
|
|
224
|
+
}
|
|
124
225
|
// Stamp schema version once.
|
|
125
226
|
const existing = db
|
|
126
227
|
.prepare("SELECT value FROM turns_meta WHERE key = 'schema_version'")
|
|
@@ -21,6 +21,29 @@ export function rowToEntry(r) {
|
|
|
21
21
|
pressureBand: r.pressure_band ?? undefined,
|
|
22
22
|
model: r.model ?? undefined,
|
|
23
23
|
epochId: r.epoch_id ?? undefined,
|
|
24
|
+
// H1: telemetry columns (only populated when the row has them non-default).
|
|
25
|
+
hyde: r.hyde_ran == null
|
|
26
|
+
? undefined
|
|
27
|
+
: {
|
|
28
|
+
ran: r.hyde_ran === 1,
|
|
29
|
+
reason: r.hyde_reason ?? "",
|
|
30
|
+
hypotheticalDoc: r.hyde_doc ?? "",
|
|
31
|
+
rawHitCount: r.hyde_raw_count ?? 0,
|
|
32
|
+
hydeHitCount: r.hyde_hyde_count ?? 0,
|
|
33
|
+
fusedHitCount: r.hyde_fused_count ?? 0,
|
|
34
|
+
lift: r.hyde_lift ?? 0,
|
|
35
|
+
generationMs: r.hyde_generation_ms ?? 0,
|
|
36
|
+
},
|
|
37
|
+
recallMetrics: r.recall_score == null
|
|
38
|
+
? undefined
|
|
39
|
+
: {
|
|
40
|
+
score: r.recall_score ?? 0,
|
|
41
|
+
pass: r.recall_pass === 1,
|
|
42
|
+
relevance: r.recall_relevance ?? 0,
|
|
43
|
+
coverage: r.recall_coverage ?? 0,
|
|
44
|
+
diversity: r.recall_diversity ?? 0,
|
|
45
|
+
specificity: r.recall_specificity ?? 0,
|
|
46
|
+
},
|
|
24
47
|
};
|
|
25
48
|
}
|
|
26
49
|
export function rowToRecall(r) {
|
|
@@ -7,9 +7,13 @@ export function appendTurn(ctx, entry) {
|
|
|
7
7
|
try {
|
|
8
8
|
db
|
|
9
9
|
.prepare(`INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
|
|
10
|
-
ctx_tokens, ctx_percent, pressure_band, model, epoch_id
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
ctx_tokens, ctx_percent, pressure_band, model, epoch_id,
|
|
11
|
+
hyde_ran, hyde_doc, hyde_raw_count, hyde_hyde_count,
|
|
12
|
+
hyde_fused_count, hyde_lift, hyde_generation_ms, hyde_reason,
|
|
13
|
+
recall_score, recall_pass, recall_relevance, recall_coverage,
|
|
14
|
+
recall_diversity, recall_specificity)
|
|
15
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
16
|
+
.run(entry.conversationId, sid, entry.turnIndex, entry.role, entry.endedAt, entry.ctxTokens ?? null, entry.ctxPercent ?? null, entry.pressureBand ?? null, entry.model ?? null, entry.epochId ?? null, entry.hyde?.ran == null ? null : entry.hyde.ran ? 1 : 0, entry.hyde?.hypotheticalDoc ?? null, entry.hyde?.rawHitCount ?? null, entry.hyde?.hydeHitCount ?? null, entry.hyde?.fusedHitCount ?? null, entry.hyde?.lift ?? null, entry.hyde?.generationMs ?? null, entry.hyde?.reason ?? null, entry.recallMetrics?.score ?? null, entry.recallMetrics?.pass == null ? null : entry.recallMetrics.pass ? 1 : 0, entry.recallMetrics?.relevance ?? null, entry.recallMetrics?.coverage ?? null, entry.recallMetrics?.diversity ?? null, entry.recallMetrics?.specificity ?? null);
|
|
13
17
|
}
|
|
14
18
|
catch (e) {
|
|
15
19
|
if (e instanceof Error &&
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { withTx } from "../store/turns/connection.js";
|
|
2
|
+
import { moveMemory, recordEvolution, } from "../wiki/curation-helpers.js";
|
|
3
|
+
/** Read all overrides in chronological order (oldest first). */
|
|
4
|
+
function loadOverrides(db) {
|
|
5
|
+
return db
|
|
6
|
+
.prepare(`SELECT topic_id, kind, custom_label, merged_into, split_from, split_memory_ids, merged_memory_ids
|
|
7
|
+
FROM topic_overrides ORDER BY overridden_at ASC`)
|
|
8
|
+
.all();
|
|
9
|
+
}
|
|
10
|
+
/** Does a topic row with this id exist? */
|
|
11
|
+
function topicExists(db, topicId) {
|
|
12
|
+
return !!db.prepare("SELECT 1 FROM topics WHERE id = ?").get(topicId);
|
|
13
|
+
}
|
|
14
|
+
/** All existing topic ids (for nearest-by-label-overlap fallback). */
|
|
15
|
+
function allTopicIds(db) {
|
|
16
|
+
const rows = db
|
|
17
|
+
.prepare("SELECT id FROM topics")
|
|
18
|
+
.all();
|
|
19
|
+
return rows.map((r) => r.id);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* When a curated target topic dissolved in a rebuild, fall back to whichever
|
|
23
|
+
* surviving topic shares the most label tokens, so merged memories still land
|
|
24
|
+
* somewhere sensible. Returns "" when no overlap candidate exists.
|
|
25
|
+
*/
|
|
26
|
+
function nearestByLabelOverlap(db, label) {
|
|
27
|
+
const tokens = new Set(label.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0));
|
|
28
|
+
if (tokens.size === 0)
|
|
29
|
+
return "";
|
|
30
|
+
let bestId = "";
|
|
31
|
+
let bestOverlap = 0;
|
|
32
|
+
for (const id of allTopicIds(db)) {
|
|
33
|
+
const r = db
|
|
34
|
+
.prepare("SELECT label FROM topics WHERE id = ?")
|
|
35
|
+
.get(id);
|
|
36
|
+
if (!r)
|
|
37
|
+
continue;
|
|
38
|
+
const labelTokens = new Set(r.label.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0));
|
|
39
|
+
let overlap = 0;
|
|
40
|
+
for (const t of tokens)
|
|
41
|
+
if (labelTokens.has(t))
|
|
42
|
+
overlap++;
|
|
43
|
+
if (overlap > bestOverlap) {
|
|
44
|
+
bestOverlap = overlap;
|
|
45
|
+
bestId = id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return bestOverlap > 0 ? bestId : "";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Replay a single merge override: force the source topic's merged members back
|
|
52
|
+
* into the target topic (or its nearest-by-label-overlap survivor if the target
|
|
53
|
+
* dissolved). Member ids are read from `merged_memory_ids` on the override row
|
|
54
|
+
* (W5 — survives `replaceTopicModel` because `topic_overrides` is never wiped).
|
|
55
|
+
* Falls back to `topic_evolution` for pre-W5 merges (only works when evolution
|
|
56
|
+
* rows are intact, i.e. the incremental path or no rebuild has fired since).
|
|
57
|
+
*/
|
|
58
|
+
function replayMerge(db, row) {
|
|
59
|
+
const target = row.merged_into;
|
|
60
|
+
if (!target)
|
|
61
|
+
return;
|
|
62
|
+
// Primary source: merged_memory_ids on the override row itself.
|
|
63
|
+
let memoryIds = [];
|
|
64
|
+
if (row.merged_memory_ids) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(row.merged_memory_ids);
|
|
67
|
+
if (Array.isArray(parsed)) {
|
|
68
|
+
memoryIds = parsed.filter((m) => typeof m === "string");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* non-fatal: malformed member list falls through to evolution */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Fallback: topic_evolution rows (only intact when no rebuild has fired).
|
|
76
|
+
let members = [];
|
|
77
|
+
if (memoryIds.length === 0) {
|
|
78
|
+
members = db
|
|
79
|
+
.prepare(`SELECT memory_id, session_id FROM topic_evolution
|
|
80
|
+
WHERE topic_id = ? AND method = 'merge' ORDER BY memory_id ASC`)
|
|
81
|
+
.all(target);
|
|
82
|
+
if (members.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Resolve session_id for each member from its current assignment (if any).
|
|
87
|
+
for (const mid of memoryIds) {
|
|
88
|
+
const r = db
|
|
89
|
+
.prepare("SELECT session_id FROM memory_topics WHERE memory_id = ? LIMIT 1")
|
|
90
|
+
.get(mid);
|
|
91
|
+
members.push({
|
|
92
|
+
memory_id: mid,
|
|
93
|
+
session_id: r?.session_id ?? null,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let dest = target;
|
|
98
|
+
if (!topicExists(db, target)) {
|
|
99
|
+
dest = nearestByLabelOverlap(db, target);
|
|
100
|
+
if (!dest)
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
withTx(db, () => {
|
|
104
|
+
const t = Date.now();
|
|
105
|
+
for (const m of members) {
|
|
106
|
+
moveMemory(db, m.memory_id, dest);
|
|
107
|
+
recordEvolution(db, dest, m.memory_id, m.session_id, t, "merge");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Replay a single split override: recreate the split topic (if the source
|
|
113
|
+
* still exists or the memories are somewhere in the model) and move the listed
|
|
114
|
+
* memory ids into it.
|
|
115
|
+
*/
|
|
116
|
+
function replaySplit(db, row) {
|
|
117
|
+
let memoryIds = [];
|
|
118
|
+
if (row.split_memory_ids) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(row.split_memory_ids);
|
|
121
|
+
if (Array.isArray(parsed)) {
|
|
122
|
+
memoryIds = parsed.filter((m) => typeof m === "string");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* non-fatal: malformed member list is skipped */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (memoryIds.length === 0 || topicExists(db, row.topic_id))
|
|
130
|
+
return;
|
|
131
|
+
// Recreate the split topic reusing its original id + source label.
|
|
132
|
+
const sourceLabel = row.split_from && topicExists(db, row.split_from)
|
|
133
|
+
? db.prepare("SELECT label FROM topics WHERE id = ?")
|
|
134
|
+
.get(row.split_from).label
|
|
135
|
+
: row.topic_id;
|
|
136
|
+
withTx(db, () => {
|
|
137
|
+
db.prepare(`INSERT INTO topics (id, label, term_scores, memory_count, last_updated, cluster_model_built_at)
|
|
138
|
+
VALUES (?, ?, NULL, 0, ?, NULL)`).run(row.topic_id, `${sourceLabel} (split)`, Date.now());
|
|
139
|
+
const t = Date.now();
|
|
140
|
+
// Only memories that still exist in the model move; drop the rest.
|
|
141
|
+
for (const memoryId of memoryIds) {
|
|
142
|
+
const src = db
|
|
143
|
+
.prepare("SELECT session_id FROM memory_topics WHERE memory_id = ? LIMIT 1")
|
|
144
|
+
.get(memoryId);
|
|
145
|
+
if (!src)
|
|
146
|
+
continue;
|
|
147
|
+
moveMemory(db, memoryId, row.topic_id);
|
|
148
|
+
recordEvolution(db, row.topic_id, memoryId, src.session_id, t, "split");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Apply all user overrides (label / merge / split) after a topic-model rebuild,
|
|
154
|
+
* in `overridden_at` order. Non-fatal end-to-end: each override is isolated so a
|
|
155
|
+
* single corruption never aborts the replay. This is the W5 high-fidelity replay (feature A).
|
|
156
|
+
*/
|
|
157
|
+
export function applyFullOverridesAfterRebuild(db) {
|
|
158
|
+
if (!db)
|
|
159
|
+
return;
|
|
160
|
+
let rows;
|
|
161
|
+
try {
|
|
162
|
+
rows = loadOverrides(db);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return; // non-fatal: no overrides table / unreadable → nothing to replay
|
|
166
|
+
}
|
|
167
|
+
for (const row of rows) {
|
|
168
|
+
try {
|
|
169
|
+
if (row.kind === "label") {
|
|
170
|
+
// Same fast path as store.applyOverridesAfterRebuild, inline here
|
|
171
|
+
// so the full replay owns label + merge + split in one pass.
|
|
172
|
+
if (row.custom_label && row.custom_label.trim() !== "") {
|
|
173
|
+
db.prepare("UPDATE topics SET label = ? WHERE id = ?").run(row.custom_label, row.topic_id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (row.kind === "merge") {
|
|
177
|
+
replayMerge(db, row);
|
|
178
|
+
}
|
|
179
|
+
else if (row.kind === "split") {
|
|
180
|
+
replaySplit(db, row);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* non-fatal: skip this override, keep replaying the rest */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/src/topics/index.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* index.ts — S51 topics barrel. Re-export only; no logic.
|
|
3
3
|
*/
|
|
4
4
|
export { loadEmbeddings, buildTopicModel, DEFAULT_CLUSTER_CONFIG, } from "./cluster.js";
|
|
5
|
-
export { createTopicStore, getWikiCompactCounter, bumpWikiCompactCounter } from "./store.js";
|
|
5
|
+
export { createTopicStore, getWikiCompactCounter, bumpWikiCompactCounter, applyOverridesAfterRebuild, } from "./store.js";
|
|
6
|
+
export { applyFullOverridesAfterRebuild } from "./durability.js";
|
|
6
7
|
export { tokenize, tfidfScores, labelFromScores, membershipConfidence } from "./labels.js";
|
package/dist/src/topics/store.js
CHANGED
|
@@ -35,10 +35,7 @@ function rowToTopic(r) {
|
|
|
35
35
|
function rowToAssignment(r) {
|
|
36
36
|
return {
|
|
37
37
|
memoryId: r.memory_id,
|
|
38
|
-
|
|
39
|
-
// enough for the wiki view (context_chunks PK is (session_id, id)).
|
|
40
|
-
// sessionId omitted — memory_topics has no session_id column;
|
|
41
|
-
// the TopicAssignment type marks it optional for this reason.
|
|
38
|
+
sessionId: r.session_id ?? "",
|
|
42
39
|
topicId: r.topic_id,
|
|
43
40
|
confidence: r.confidence ?? 0,
|
|
44
41
|
assignedAt: r.assigned_at ?? 0,
|
|
@@ -57,10 +54,10 @@ export function createTopicStore(stateDir) {
|
|
|
57
54
|
for (const t of model.topics) {
|
|
58
55
|
insTopic.run(t.id, t.label, JSON.stringify(t.termScores), t.memoryCount, t.lastUpdated, model.builtAt);
|
|
59
56
|
}
|
|
60
|
-
const insAssign = db.prepare(`INSERT OR REPLACE INTO memory_topics (memory_id, topic_id, confidence, assigned_at, method)
|
|
61
|
-
VALUES (?, ?, ?, ?, 'kmeans+tfidf')`);
|
|
57
|
+
const insAssign = db.prepare(`INSERT OR REPLACE INTO memory_topics (memory_id, topic_id, confidence, assigned_at, method, session_id)
|
|
58
|
+
VALUES (?, ?, ?, ?, 'kmeans+tfidf', ?)`);
|
|
62
59
|
for (const a of model.assignments) {
|
|
63
|
-
insAssign.run(a.memoryId, a.topicId, a.confidence, a.assignedAt);
|
|
60
|
+
insAssign.run(a.memoryId, a.topicId, a.confidence, a.assignedAt, a.sessionId ?? null);
|
|
64
61
|
}
|
|
65
62
|
});
|
|
66
63
|
}
|
|
@@ -74,7 +71,7 @@ export function createTopicStore(stateDir) {
|
|
|
74
71
|
},
|
|
75
72
|
getMemoriesForTopic(topicId, limit = 50, offset = 0) {
|
|
76
73
|
const rows = db
|
|
77
|
-
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
74
|
+
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method, session_id
|
|
78
75
|
FROM memory_topics WHERE topic_id = ?
|
|
79
76
|
ORDER BY confidence DESC, memory_id ASC LIMIT ? OFFSET ?`)
|
|
80
77
|
.all(topicId, limit, offset);
|
|
@@ -82,7 +79,7 @@ export function createTopicStore(stateDir) {
|
|
|
82
79
|
},
|
|
83
80
|
getTopicForMemory(memoryId) {
|
|
84
81
|
const r = db
|
|
85
|
-
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
82
|
+
.prepare(`SELECT memory_id, topic_id, confidence, assigned_at, method, session_id
|
|
86
83
|
FROM memory_topics WHERE memory_id = ? LIMIT 1`)
|
|
87
84
|
.get(memoryId);
|
|
88
85
|
return r ? rowToAssignment(r) : null;
|
|
@@ -101,6 +98,28 @@ export function createTopicStore(stateDir) {
|
|
|
101
98
|
},
|
|
102
99
|
};
|
|
103
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Re-apply user label overrides after a topic-model rebuild. `replaceTopicModel`
|
|
103
|
+
* deletes + recreates `topics`, so any `label` override rows that resolved to a
|
|
104
|
+
* custom label would otherwise be lost — this copies them back onto the fresh
|
|
105
|
+
* rows. Non-fatal: a failure never breaks a rebuild.
|
|
106
|
+
*/
|
|
107
|
+
export function applyOverridesAfterRebuild(db) {
|
|
108
|
+
try {
|
|
109
|
+
const rows = db
|
|
110
|
+
.prepare("SELECT topic_id, custom_label FROM topic_overrides WHERE kind = 'label'")
|
|
111
|
+
.all();
|
|
112
|
+
const upd = db.prepare("UPDATE topics SET label = ? WHERE id = ?");
|
|
113
|
+
for (const r of rows) {
|
|
114
|
+
if (r.custom_label && r.custom_label.trim() !== "") {
|
|
115
|
+
upd.run(r.custom_label, r.topic_id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* non-fatal: override re-apply never breaks a rebuild */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
104
123
|
/** Turns-meta counter key for the every-Nth-compaction rebuild trigger. */
|
|
105
124
|
const WIKI_COUNTER_KEY = "wiki_compact_counter";
|
|
106
125
|
/** Read the rebuild counter (0 when unset). */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Read a topic's current label, or null when the topic does not exist. */
|
|
2
|
+
export function getTopicLabel(db, topicId) {
|
|
3
|
+
const r = db
|
|
4
|
+
.prepare("SELECT label FROM topics WHERE id = ?")
|
|
5
|
+
.get(topicId);
|
|
6
|
+
return r?.label ?? null;
|
|
7
|
+
}
|
|
8
|
+
/** Recompute memory_count for a topic from its live member rows. */
|
|
9
|
+
export function recountTopic(db, topicId) {
|
|
10
|
+
const c = db
|
|
11
|
+
.prepare("SELECT COUNT(*) AS c FROM memory_topics WHERE topic_id = ?")
|
|
12
|
+
.get(topicId);
|
|
13
|
+
db.prepare("UPDATE topics SET memory_count = ? WHERE id = ?").run(c.c, topicId);
|
|
14
|
+
}
|
|
15
|
+
/** Look up a custom label override; empty/blank means "no override". */
|
|
16
|
+
export function customLabelOverride(db, topicId) {
|
|
17
|
+
const r = db
|
|
18
|
+
.prepare(`SELECT custom_label FROM topic_overrides
|
|
19
|
+
WHERE topic_id = ? AND kind = 'label'`)
|
|
20
|
+
.get(topicId);
|
|
21
|
+
const label = r?.custom_label;
|
|
22
|
+
return label && label.trim() !== "" ? label : null;
|
|
23
|
+
}
|
|
24
|
+
/** Write (or clear) a label override for a topic. */
|
|
25
|
+
export function writeLabelOverride(db, topicId, label, now) {
|
|
26
|
+
if (label.trim() === "") {
|
|
27
|
+
db.prepare("DELETE FROM topic_overrides WHERE topic_id = ? AND kind = 'label'").run(topicId);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
db.prepare(`INSERT OR REPLACE INTO topic_overrides
|
|
31
|
+
(topic_id, kind, custom_label, overridden_at)
|
|
32
|
+
VALUES (?, 'label', ?, ?)`).run(topicId, label, now);
|
|
33
|
+
// Keep the live topics.label in sync with the override so reads resolve directly.
|
|
34
|
+
db.prepare("UPDATE topics SET label = ? WHERE id = ?").run(label, topicId);
|
|
35
|
+
}
|
|
36
|
+
/** Copy a source assignment into the target topic (idempotent, atomic caller). */
|
|
37
|
+
export function moveMemory(db, memoryId, toTopicId) {
|
|
38
|
+
const src = db
|
|
39
|
+
.prepare(`SELECT topic_id, confidence, assigned_at, session_id FROM memory_topics
|
|
40
|
+
WHERE memory_id = ? LIMIT 1`)
|
|
41
|
+
.get(memoryId);
|
|
42
|
+
// Delete the source row first so a memory already present in the target just stays.
|
|
43
|
+
db.prepare("DELETE FROM memory_topics WHERE memory_id = ?").run(memoryId);
|
|
44
|
+
if (!src)
|
|
45
|
+
return;
|
|
46
|
+
db.prepare(`INSERT OR REPLACE INTO memory_topics
|
|
47
|
+
(memory_id, topic_id, confidence, assigned_at, method, session_id)
|
|
48
|
+
VALUES (?, ?, ?, ?, 'kmeans+tfidf', ?)`).run(memoryId, toTopicId, src.confidence ?? 0, src.assigned_at ?? 0, src.session_id ?? null);
|
|
49
|
+
}
|
|
50
|
+
/** Record a member's provenance evolution row (best-effort, non-fatal). */
|
|
51
|
+
export function recordEvolution(db, topicId, memoryId, sessionId, assignedAt, method) {
|
|
52
|
+
db.prepare(`INSERT OR REPLACE INTO topic_evolution
|
|
53
|
+
(topic_id, memory_id, session_id, assigned_at, method)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?)`).run(topicId, memoryId, sessionId, assignedAt, method);
|
|
55
|
+
}
|
|
56
|
+
/** Create a new split topic, returning its id. */
|
|
57
|
+
export function createSplitTopic(db, sourceId, sourceLabel, now) {
|
|
58
|
+
// Two splits in the same millisecond collide on the base id; append a
|
|
59
|
+
// numeric suffix until the id is unused.
|
|
60
|
+
let newId = `topic_${sourceId}_split_${now}`;
|
|
61
|
+
let suffix = 2;
|
|
62
|
+
let exists = db.prepare("SELECT 1 FROM topics WHERE id = ?").get(newId);
|
|
63
|
+
while (exists) {
|
|
64
|
+
newId = `topic_${sourceId}_split_${now}_${suffix}`;
|
|
65
|
+
exists = db.prepare("SELECT 1 FROM topics WHERE id = ?").get(newId);
|
|
66
|
+
suffix++;
|
|
67
|
+
}
|
|
68
|
+
db.prepare(`INSERT INTO topics (id, label, term_scores, memory_count, last_updated, cluster_model_built_at)
|
|
69
|
+
VALUES (?, ?, NULL, 0, ?, NULL)`).run(newId, `${sourceLabel} (split)`, now);
|
|
70
|
+
return newId;
|
|
71
|
+
}
|