pi-mega-compact 0.13.5 → 0.13.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.
- 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/index-reader.js +3 -1
- package/dist/extensions/dashboard-server/resolve-snapshot.js +81 -0
- package/dist/extensions/dashboard-server/routes-core.js +8 -3
- 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-repo.js +5 -1
- package/dist/extensions/dashboard-server/routes-sessions.js +6 -3
- 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-runtime/capture-model.js +5 -2
- 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/pricing.js +8 -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-j2VOSRp6.js +70 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C30AwyoF.js.map → AreaChart-j2VOSRp6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Dr1HeMfc.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Dr1HeMfc.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-DRG2k7FS.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-DRG2k7FS.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-DGE9PpW3.js +2 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-DGE9PpW3.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DeYRhF4q.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DeYRhF4q.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DSnSO3nG.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DSnSO3nG.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-RIzNc1Uj.js +2 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-RIzNc1Uj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-_1BJ4Bmh.js +2 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-_1BJ4Bmh.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-DkU7ywZB.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-DkU7ywZB.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-Bq_F_aOk.js +2 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-Bq_F_aOk.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-BkBD2Y97.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-BkBD2Y97.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-BXzFKU7C.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-BXzFKU7C.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-Bjpp3vBH.js +5 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-Bjpp3vBH.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-dBN2EA5G.js +2 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-dBN2EA5G.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/button-Bt0b5QVp.js +2 -0
- package/extensions/dashboard-client/dist/assets/button-Bt0b5QVp.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/card-D5CafXtg.js +2 -0
- package/extensions/dashboard-client/dist/assets/card-D5CafXtg.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +1 -0
- package/extensions/dashboard-client/dist/assets/index-GPHRnEN7.js +137 -0
- package/extensions/dashboard-client/dist/assets/index-GPHRnEN7.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/pricing-DkYp8-vi.js +2 -0
- package/extensions/dashboard-client/dist/assets/pricing-DkYp8-vi.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/switch-CY5Gwj1o.js +2 -0
- package/extensions/dashboard-client/dist/assets/switch-CY5Gwj1o.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/toggle-C9HpkIfH.js +2 -0
- package/extensions/dashboard-client/dist/assets/toggle-C9HpkIfH.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js → useSSE-2JFrLDlE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js.map → useSSE-2JFrLDlE.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/index-reader.ts +3 -1
- package/extensions/dashboard-server/resolve-snapshot.ts +98 -0
- package/extensions/dashboard-server/routes-core.ts +8 -3
- 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-repo.ts +5 -1
- package/extensions/dashboard-server/routes-sessions.ts +6 -3
- 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-runtime/capture-model.ts +5 -2
- 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/pricing.ts +8 -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/dist/assets/pricing-CvE4NllQ.js +0 -2
- package/extensions/dashboard-client/dist/assets/pricing-CvE4NllQ.js.map +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
|
@@ -22,8 +22,12 @@ export function appendTurn(ctx: SqliteTurnStoreCtx, entry: TurnEntry): TurnId {
|
|
|
22
22
|
db
|
|
23
23
|
.prepare(
|
|
24
24
|
`INSERT INTO turns (conversation_id, session_id, turn_index, role, ended_at,
|
|
25
|
-
ctx_tokens, ctx_percent, pressure_band, model, epoch_id
|
|
26
|
-
|
|
25
|
+
ctx_tokens, ctx_percent, pressure_band, model, epoch_id,
|
|
26
|
+
hyde_ran, hyde_doc, hyde_raw_count, hyde_hyde_count,
|
|
27
|
+
hyde_fused_count, hyde_lift, hyde_generation_ms, hyde_reason,
|
|
28
|
+
recall_score, recall_pass, recall_relevance, recall_coverage,
|
|
29
|
+
recall_diversity, recall_specificity)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
27
31
|
)
|
|
28
32
|
.run(
|
|
29
33
|
entry.conversationId,
|
|
@@ -36,6 +40,20 @@ export function appendTurn(ctx: SqliteTurnStoreCtx, entry: TurnEntry): TurnId {
|
|
|
36
40
|
entry.pressureBand ?? null,
|
|
37
41
|
entry.model ?? null,
|
|
38
42
|
entry.epochId ?? null,
|
|
43
|
+
entry.hyde?.ran == null ? null : entry.hyde.ran ? 1 : 0,
|
|
44
|
+
entry.hyde?.hypotheticalDoc ?? null,
|
|
45
|
+
entry.hyde?.rawHitCount ?? null,
|
|
46
|
+
entry.hyde?.hydeHitCount ?? null,
|
|
47
|
+
entry.hyde?.fusedHitCount ?? null,
|
|
48
|
+
entry.hyde?.lift ?? null,
|
|
49
|
+
entry.hyde?.generationMs ?? null,
|
|
50
|
+
entry.hyde?.reason ?? null,
|
|
51
|
+
entry.recallMetrics?.score ?? null,
|
|
52
|
+
entry.recallMetrics?.pass == null ? null : entry.recallMetrics.pass ? 1 : 0,
|
|
53
|
+
entry.recallMetrics?.relevance ?? null,
|
|
54
|
+
entry.recallMetrics?.coverage ?? null,
|
|
55
|
+
entry.recallMetrics?.diversity ?? null,
|
|
56
|
+
entry.recallMetrics?.specificity ?? null,
|
|
39
57
|
);
|
|
40
58
|
} catch (e) {
|
|
41
59
|
if (
|
package/src/store/turns/types.ts
CHANGED
|
@@ -59,6 +59,39 @@ export interface TurnEntry {
|
|
|
59
59
|
model?: string; // model used for this turn
|
|
60
60
|
/** S50B: the compact epoch that superseded this turn (stamped post-hoc). */
|
|
61
61
|
epochId?: string;
|
|
62
|
+
/** H1: HyDE invocation telemetry for this turn (persisted to hyde_* columns). */
|
|
63
|
+
hyde?: TurnHydeTelemetry;
|
|
64
|
+
/** H1: recall-quality snapshot for this turn (persisted to recall_* columns). */
|
|
65
|
+
recallMetrics?: TurnRecallTelemetry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* H1: HyDE telemetry for a turn — a structural subset of the recall-core
|
|
70
|
+
* `HydeInvocationInfo` that maps onto the turns `hyde_*` columns.
|
|
71
|
+
*/
|
|
72
|
+
export interface TurnHydeTelemetry {
|
|
73
|
+
ran: boolean;
|
|
74
|
+
/** Skip reason when HyDE didn't run: "disabled" | "no-llm" | "generation-failed". */
|
|
75
|
+
reason: string;
|
|
76
|
+
hypotheticalDoc: string;
|
|
77
|
+
rawHitCount: number;
|
|
78
|
+
hydeHitCount: number;
|
|
79
|
+
fusedHitCount: number;
|
|
80
|
+
lift: number;
|
|
81
|
+
generationMs: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* H1: recall-quality snapshot for a turn — a structural subset of the recall-core
|
|
86
|
+
* `RecallMetricsSnapshot` that maps onto the turns `recall_*` columns.
|
|
87
|
+
*/
|
|
88
|
+
export interface TurnRecallTelemetry {
|
|
89
|
+
score: number;
|
|
90
|
+
pass: boolean;
|
|
91
|
+
relevance: number;
|
|
92
|
+
coverage: number;
|
|
93
|
+
diversity: number;
|
|
94
|
+
specificity: number;
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
/** A recall hit recorded during a turn — an immutable, append-only fact. */
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* durability.ts — W5 full user-override replay over the topic tables.
|
|
3
|
+
*
|
|
4
|
+
* `replaceTopicModel` rebuilds `topics` + `memory_topics` from scratch, which
|
|
5
|
+
* would silently discard user curation (rename / merge / split). The small
|
|
6
|
+
* `applyOverridesAfterRebuild` in store.ts only replays label overrides; this
|
|
7
|
+
* module is the comprehensive replay — it replays label, merge, AND split
|
|
8
|
+
* overrides in chronological (`overridden_at`) order so the topic model stays
|
|
9
|
+
* durable across rebuilds and the incremental-assignment path (wiki/incremental.ts).
|
|
10
|
+
*
|
|
11
|
+
* Host-agnostic (no pi imports), pure local node:sqlite (PREVENT-PI-004), all
|
|
12
|
+
* SQL parameterized (PREVENT-002). Every override step is wrapped in try/catch
|
|
13
|
+
* and best-effort — a single bad row never aborts the rest of the replay.
|
|
14
|
+
*/
|
|
15
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
16
|
+
import { withTx } from "../store/turns/connection.js";
|
|
17
|
+
import {
|
|
18
|
+
moveMemory,
|
|
19
|
+
recordEvolution,
|
|
20
|
+
} from "../wiki/curation-helpers.js";
|
|
21
|
+
|
|
22
|
+
/** One `topic_overrides` row ordered by override time. */
|
|
23
|
+
interface OverrideRow {
|
|
24
|
+
topic_id: string;
|
|
25
|
+
kind: "label" | "merge" | "split";
|
|
26
|
+
custom_label: string | null;
|
|
27
|
+
merged_into: string | null;
|
|
28
|
+
split_from: string | null;
|
|
29
|
+
split_memory_ids: string | null;
|
|
30
|
+
merged_memory_ids: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read all overrides in chronological order (oldest first). */
|
|
34
|
+
function loadOverrides(db: DatabaseSync): OverrideRow[] {
|
|
35
|
+
return db
|
|
36
|
+
.prepare(
|
|
37
|
+
`SELECT topic_id, kind, custom_label, merged_into, split_from, split_memory_ids, merged_memory_ids
|
|
38
|
+
FROM topic_overrides ORDER BY overridden_at ASC`,
|
|
39
|
+
)
|
|
40
|
+
.all() as unknown as OverrideRow[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Does a topic row with this id exist? */
|
|
44
|
+
function topicExists(db: DatabaseSync, topicId: string): boolean {
|
|
45
|
+
return !!db.prepare("SELECT 1 FROM topics WHERE id = ?").get(topicId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** All existing topic ids (for nearest-by-label-overlap fallback). */
|
|
49
|
+
function allTopicIds(db: DatabaseSync): string[] {
|
|
50
|
+
const rows = db
|
|
51
|
+
.prepare("SELECT id FROM topics")
|
|
52
|
+
.all() as Array<{ id: string }>;
|
|
53
|
+
return rows.map((r) => r.id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* When a curated target topic dissolved in a rebuild, fall back to whichever
|
|
58
|
+
* surviving topic shares the most label tokens, so merged memories still land
|
|
59
|
+
* somewhere sensible. Returns "" when no overlap candidate exists.
|
|
60
|
+
*/
|
|
61
|
+
function nearestByLabelOverlap(db: DatabaseSync, label: string): string {
|
|
62
|
+
const tokens = new Set(
|
|
63
|
+
label.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0),
|
|
64
|
+
);
|
|
65
|
+
if (tokens.size === 0) return "";
|
|
66
|
+
let bestId = "";
|
|
67
|
+
let bestOverlap = 0;
|
|
68
|
+
for (const id of allTopicIds(db)) {
|
|
69
|
+
const r = db
|
|
70
|
+
.prepare("SELECT label FROM topics WHERE id = ?")
|
|
71
|
+
.get(id) as { label: string } | undefined;
|
|
72
|
+
if (!r) continue;
|
|
73
|
+
const labelTokens = new Set(
|
|
74
|
+
r.label.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0),
|
|
75
|
+
);
|
|
76
|
+
let overlap = 0;
|
|
77
|
+
for (const t of tokens) if (labelTokens.has(t)) overlap++;
|
|
78
|
+
if (overlap > bestOverlap) {
|
|
79
|
+
bestOverlap = overlap;
|
|
80
|
+
bestId = id;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return bestOverlap > 0 ? bestId : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Replay a single merge override: force the source topic's merged members back
|
|
88
|
+
* into the target topic (or its nearest-by-label-overlap survivor if the target
|
|
89
|
+
* dissolved). Member ids are read from `merged_memory_ids` on the override row
|
|
90
|
+
* (W5 — survives `replaceTopicModel` because `topic_overrides` is never wiped).
|
|
91
|
+
* Falls back to `topic_evolution` for pre-W5 merges (only works when evolution
|
|
92
|
+
* rows are intact, i.e. the incremental path or no rebuild has fired since).
|
|
93
|
+
*/
|
|
94
|
+
function replayMerge(
|
|
95
|
+
db: DatabaseSync,
|
|
96
|
+
row: OverrideRow,
|
|
97
|
+
): void {
|
|
98
|
+
const target = row.merged_into;
|
|
99
|
+
if (!target) return;
|
|
100
|
+
// Primary source: merged_memory_ids on the override row itself.
|
|
101
|
+
let memoryIds: string[] = [];
|
|
102
|
+
if (row.merged_memory_ids) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(row.merged_memory_ids) as unknown;
|
|
105
|
+
if (Array.isArray(parsed)) {
|
|
106
|
+
memoryIds = parsed.filter(
|
|
107
|
+
(m): m is string => typeof m === "string",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
/* non-fatal: malformed member list falls through to evolution */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Fallback: topic_evolution rows (only intact when no rebuild has fired).
|
|
115
|
+
let members: Array<{ memory_id: string; session_id: string | null }> = [];
|
|
116
|
+
if (memoryIds.length === 0) {
|
|
117
|
+
members = db
|
|
118
|
+
.prepare(
|
|
119
|
+
`SELECT memory_id, session_id FROM topic_evolution
|
|
120
|
+
WHERE topic_id = ? AND method = 'merge' ORDER BY memory_id ASC`,
|
|
121
|
+
)
|
|
122
|
+
.all(target) as Array<{ memory_id: string; session_id: string | null }>;
|
|
123
|
+
if (members.length === 0) return;
|
|
124
|
+
} else {
|
|
125
|
+
// Resolve session_id for each member from its current assignment (if any).
|
|
126
|
+
for (const mid of memoryIds) {
|
|
127
|
+
const r = db
|
|
128
|
+
.prepare(
|
|
129
|
+
"SELECT session_id FROM memory_topics WHERE memory_id = ? LIMIT 1",
|
|
130
|
+
)
|
|
131
|
+
.get(mid) as { session_id: string | null } | undefined;
|
|
132
|
+
members.push({
|
|
133
|
+
memory_id: mid,
|
|
134
|
+
session_id: r?.session_id ?? null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let dest = target;
|
|
139
|
+
if (!topicExists(db, target)) {
|
|
140
|
+
dest = nearestByLabelOverlap(db, target);
|
|
141
|
+
if (!dest) return;
|
|
142
|
+
}
|
|
143
|
+
withTx(db, () => {
|
|
144
|
+
const t = Date.now();
|
|
145
|
+
for (const m of members) {
|
|
146
|
+
moveMemory(db, m.memory_id, dest);
|
|
147
|
+
recordEvolution(db, dest, m.memory_id, m.session_id, t, "merge");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Replay a single split override: recreate the split topic (if the source
|
|
154
|
+
* still exists or the memories are somewhere in the model) and move the listed
|
|
155
|
+
* memory ids into it.
|
|
156
|
+
*/
|
|
157
|
+
function replaySplit(db: DatabaseSync, row: OverrideRow): void {
|
|
158
|
+
let memoryIds: string[] = [];
|
|
159
|
+
if (row.split_memory_ids) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(row.split_memory_ids) as unknown;
|
|
162
|
+
if (Array.isArray(parsed)) {
|
|
163
|
+
memoryIds = parsed.filter(
|
|
164
|
+
(m): m is string => typeof m === "string",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
/* non-fatal: malformed member list is skipped */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (memoryIds.length === 0 || topicExists(db, row.topic_id)) return;
|
|
172
|
+
// Recreate the split topic reusing its original id + source label.
|
|
173
|
+
const sourceLabel =
|
|
174
|
+
row.split_from && topicExists(db, row.split_from)
|
|
175
|
+
? (db.prepare("SELECT label FROM topics WHERE id = ?")
|
|
176
|
+
.get(row.split_from) as { label: string }).label
|
|
177
|
+
: row.topic_id;
|
|
178
|
+
withTx(db, () => {
|
|
179
|
+
db.prepare(
|
|
180
|
+
`INSERT INTO topics (id, label, term_scores, memory_count, last_updated, cluster_model_built_at)
|
|
181
|
+
VALUES (?, ?, NULL, 0, ?, NULL)`,
|
|
182
|
+
).run(row.topic_id, `${sourceLabel} (split)`, Date.now());
|
|
183
|
+
const t = Date.now();
|
|
184
|
+
// Only memories that still exist in the model move; drop the rest.
|
|
185
|
+
for (const memoryId of memoryIds) {
|
|
186
|
+
const src = db
|
|
187
|
+
.prepare(
|
|
188
|
+
"SELECT session_id FROM memory_topics WHERE memory_id = ? LIMIT 1",
|
|
189
|
+
)
|
|
190
|
+
.get(memoryId) as { session_id: string | null } | undefined;
|
|
191
|
+
if (!src) continue;
|
|
192
|
+
moveMemory(db, memoryId, row.topic_id);
|
|
193
|
+
recordEvolution(db, row.topic_id, memoryId, src.session_id, t, "split");
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Apply all user overrides (label / merge / split) after a topic-model rebuild,
|
|
200
|
+
* in `overridden_at` order. Non-fatal end-to-end: each override is isolated so a
|
|
201
|
+
* single corruption never aborts the replay. This is the W5 high-fidelity replay (feature A).
|
|
202
|
+
*/
|
|
203
|
+
export function applyFullOverridesAfterRebuild(db: DatabaseSync): void {
|
|
204
|
+
if (!db) return;
|
|
205
|
+
let rows: OverrideRow[];
|
|
206
|
+
try {
|
|
207
|
+
rows = loadOverrides(db);
|
|
208
|
+
} catch {
|
|
209
|
+
return; // non-fatal: no overrides table / unreadable → nothing to replay
|
|
210
|
+
}
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
try {
|
|
213
|
+
if (row.kind === "label") {
|
|
214
|
+
// Same fast path as store.applyOverridesAfterRebuild, inline here
|
|
215
|
+
// so the full replay owns label + merge + split in one pass.
|
|
216
|
+
if (row.custom_label && row.custom_label.trim() !== "") {
|
|
217
|
+
db.prepare("UPDATE topics SET label = ? WHERE id = ?").run(
|
|
218
|
+
row.custom_label,
|
|
219
|
+
row.topic_id,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
} else if (row.kind === "merge") {
|
|
223
|
+
replayMerge(db, row);
|
|
224
|
+
} else if (row.kind === "split") {
|
|
225
|
+
replaySplit(db, row);
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
/* non-fatal: skip this override, keep replaying the rest */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/topics/index.ts
CHANGED
|
@@ -7,7 +7,13 @@ export {
|
|
|
7
7
|
DEFAULT_CLUSTER_CONFIG,
|
|
8
8
|
type WikiClusterConfig,
|
|
9
9
|
} from "./cluster.js";
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createTopicStore,
|
|
12
|
+
getWikiCompactCounter,
|
|
13
|
+
bumpWikiCompactCounter,
|
|
14
|
+
applyOverridesAfterRebuild,
|
|
15
|
+
} from "./store.js";
|
|
16
|
+
export { applyFullOverridesAfterRebuild } from "./durability.js";
|
|
11
17
|
export type { TopicStore, StoredTopic } from "./store.js";
|
|
12
18
|
export { tokenize, tfidfScores, labelFromScores, membershipConfidence } from "./labels.js";
|
|
13
19
|
export type {
|
package/src/topics/store.ts
CHANGED
|
@@ -55,6 +55,7 @@ interface AssignmentRow {
|
|
|
55
55
|
confidence: number | null;
|
|
56
56
|
assigned_at: number | null;
|
|
57
57
|
method: string | null;
|
|
58
|
+
session_id: string | null;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function parseTermScores(
|
|
@@ -85,10 +86,7 @@ function rowToTopic(r: TopicRow): StoredTopic {
|
|
|
85
86
|
function rowToAssignment(r: AssignmentRow): TopicAssignment {
|
|
86
87
|
return {
|
|
87
88
|
memoryId: r.memory_id,
|
|
88
|
-
|
|
89
|
-
// enough for the wiki view (context_chunks PK is (session_id, id)).
|
|
90
|
-
// sessionId omitted — memory_topics has no session_id column;
|
|
91
|
-
// the TopicAssignment type marks it optional for this reason.
|
|
89
|
+
sessionId: r.session_id ?? "",
|
|
92
90
|
topicId: r.topic_id,
|
|
93
91
|
confidence: r.confidence ?? 0,
|
|
94
92
|
assignedAt: r.assigned_at ?? 0,
|
|
@@ -119,11 +117,17 @@ export function createTopicStore(stateDir?: string): TopicStore {
|
|
|
119
117
|
);
|
|
120
118
|
}
|
|
121
119
|
const insAssign = db.prepare(
|
|
122
|
-
`INSERT OR REPLACE INTO memory_topics (memory_id, topic_id, confidence, assigned_at, method)
|
|
123
|
-
VALUES (?, ?, ?, ?, 'kmeans+tfidf')`,
|
|
120
|
+
`INSERT OR REPLACE INTO memory_topics (memory_id, topic_id, confidence, assigned_at, method, session_id)
|
|
121
|
+
VALUES (?, ?, ?, ?, 'kmeans+tfidf', ?)`,
|
|
124
122
|
);
|
|
125
123
|
for (const a of model.assignments) {
|
|
126
|
-
insAssign.run(
|
|
124
|
+
insAssign.run(
|
|
125
|
+
a.memoryId,
|
|
126
|
+
a.topicId,
|
|
127
|
+
a.confidence,
|
|
128
|
+
a.assignedAt,
|
|
129
|
+
a.sessionId ?? null,
|
|
130
|
+
);
|
|
127
131
|
}
|
|
128
132
|
});
|
|
129
133
|
}
|
|
@@ -145,7 +149,7 @@ export function createTopicStore(stateDir?: string): TopicStore {
|
|
|
145
149
|
): TopicAssignment[] {
|
|
146
150
|
const rows = db
|
|
147
151
|
.prepare(
|
|
148
|
-
`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
152
|
+
`SELECT memory_id, topic_id, confidence, assigned_at, method, session_id
|
|
149
153
|
FROM memory_topics WHERE topic_id = ?
|
|
150
154
|
ORDER BY confidence DESC, memory_id ASC LIMIT ? OFFSET ?`,
|
|
151
155
|
)
|
|
@@ -156,7 +160,7 @@ export function createTopicStore(stateDir?: string): TopicStore {
|
|
|
156
160
|
getTopicForMemory(memoryId: string): TopicAssignment | null {
|
|
157
161
|
const r = db
|
|
158
162
|
.prepare(
|
|
159
|
-
`SELECT memory_id, topic_id, confidence, assigned_at, method
|
|
163
|
+
`SELECT memory_id, topic_id, confidence, assigned_at, method, session_id
|
|
160
164
|
FROM memory_topics WHERE memory_id = ? LIMIT 1`,
|
|
161
165
|
)
|
|
162
166
|
.get(memoryId) as unknown as AssignmentRow | undefined;
|
|
@@ -186,6 +190,28 @@ export function createTopicStore(stateDir?: string): TopicStore {
|
|
|
186
190
|
};
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Re-apply user label overrides after a topic-model rebuild. `replaceTopicModel`
|
|
195
|
+
* deletes + recreates `topics`, so any `label` override rows that resolved to a
|
|
196
|
+
* custom label would otherwise be lost — this copies them back onto the fresh
|
|
197
|
+
* rows. Non-fatal: a failure never breaks a rebuild.
|
|
198
|
+
*/
|
|
199
|
+
export function applyOverridesAfterRebuild(db: DatabaseSync): void {
|
|
200
|
+
try {
|
|
201
|
+
const rows = db
|
|
202
|
+
.prepare("SELECT topic_id, custom_label FROM topic_overrides WHERE kind = 'label'")
|
|
203
|
+
.all() as Array<{ topic_id: string; custom_label: string | null }>;
|
|
204
|
+
const upd = db.prepare("UPDATE topics SET label = ? WHERE id = ?");
|
|
205
|
+
for (const r of rows) {
|
|
206
|
+
if (r.custom_label && r.custom_label.trim() !== "") {
|
|
207
|
+
upd.run(r.custom_label, r.topic_id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
/* non-fatal: override re-apply never breaks a rebuild */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
189
215
|
/** Turns-meta counter key for the every-Nth-compaction rebuild trigger. */
|
|
190
216
|
const WIKI_COUNTER_KEY = "wiki_compact_counter";
|
|
191
217
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curation-helpers.ts — private DB helpers for the wiki curation store.
|
|
3
|
+
*
|
|
4
|
+
* Pure node:sqlite primitives shared by the rename/merge/split transactions in
|
|
5
|
+
* curation.ts (the factory shell). Parameterized (PREVENT-002), host-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
8
|
+
|
|
9
|
+
interface LabelRow {
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
interface CountRow {
|
|
13
|
+
c: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read a topic's current label, or null when the topic does not exist. */
|
|
17
|
+
export function getTopicLabel(db: DatabaseSync, topicId: string): string | null {
|
|
18
|
+
const r = db
|
|
19
|
+
.prepare("SELECT label FROM topics WHERE id = ?")
|
|
20
|
+
.get(topicId) as LabelRow | undefined;
|
|
21
|
+
return r?.label ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Recompute memory_count for a topic from its live member rows. */
|
|
25
|
+
export function recountTopic(db: DatabaseSync, topicId: string): void {
|
|
26
|
+
const c = db
|
|
27
|
+
.prepare("SELECT COUNT(*) AS c FROM memory_topics WHERE topic_id = ?")
|
|
28
|
+
.get(topicId) as unknown as CountRow;
|
|
29
|
+
db.prepare("UPDATE topics SET memory_count = ? WHERE id = ?").run(c.c, topicId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Look up a custom label override; empty/blank means "no override". */
|
|
33
|
+
export function customLabelOverride(
|
|
34
|
+
db: DatabaseSync,
|
|
35
|
+
topicId: string,
|
|
36
|
+
): string | null {
|
|
37
|
+
const r = db
|
|
38
|
+
.prepare(
|
|
39
|
+
`SELECT custom_label FROM topic_overrides
|
|
40
|
+
WHERE topic_id = ? AND kind = 'label'`,
|
|
41
|
+
)
|
|
42
|
+
.get(topicId) as { custom_label: string | null } | undefined;
|
|
43
|
+
const label = r?.custom_label;
|
|
44
|
+
return label && label.trim() !== "" ? label : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Write (or clear) a label override for a topic. */
|
|
48
|
+
export function writeLabelOverride(
|
|
49
|
+
db: DatabaseSync,
|
|
50
|
+
topicId: string,
|
|
51
|
+
label: string,
|
|
52
|
+
now: number,
|
|
53
|
+
): void {
|
|
54
|
+
if (label.trim() === "") {
|
|
55
|
+
db.prepare(
|
|
56
|
+
"DELETE FROM topic_overrides WHERE topic_id = ? AND kind = 'label'",
|
|
57
|
+
).run(topicId);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
db.prepare(
|
|
61
|
+
`INSERT OR REPLACE INTO topic_overrides
|
|
62
|
+
(topic_id, kind, custom_label, overridden_at)
|
|
63
|
+
VALUES (?, 'label', ?, ?)`,
|
|
64
|
+
).run(topicId, label, now);
|
|
65
|
+
// Keep the live topics.label in sync with the override so reads resolve directly.
|
|
66
|
+
db.prepare("UPDATE topics SET label = ? WHERE id = ?").run(label, topicId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Copy a source assignment into the target topic (idempotent, atomic caller). */
|
|
70
|
+
export function moveMemory(
|
|
71
|
+
db: DatabaseSync,
|
|
72
|
+
memoryId: string,
|
|
73
|
+
toTopicId: string,
|
|
74
|
+
): void {
|
|
75
|
+
const src = db
|
|
76
|
+
.prepare(
|
|
77
|
+
`SELECT topic_id, confidence, assigned_at, session_id FROM memory_topics
|
|
78
|
+
WHERE memory_id = ? LIMIT 1`,
|
|
79
|
+
)
|
|
80
|
+
.get(memoryId) as {
|
|
81
|
+
topic_id: string;
|
|
82
|
+
confidence: number | null;
|
|
83
|
+
assigned_at: number | null;
|
|
84
|
+
session_id: string | null;
|
|
85
|
+
} | undefined;
|
|
86
|
+
// Delete the source row first so a memory already present in the target just stays.
|
|
87
|
+
db.prepare("DELETE FROM memory_topics WHERE memory_id = ?").run(memoryId);
|
|
88
|
+
if (!src) return;
|
|
89
|
+
db.prepare(
|
|
90
|
+
`INSERT OR REPLACE INTO memory_topics
|
|
91
|
+
(memory_id, topic_id, confidence, assigned_at, method, session_id)
|
|
92
|
+
VALUES (?, ?, ?, ?, 'kmeans+tfidf', ?)`,
|
|
93
|
+
).run(
|
|
94
|
+
memoryId,
|
|
95
|
+
toTopicId,
|
|
96
|
+
src.confidence ?? 0,
|
|
97
|
+
src.assigned_at ?? 0,
|
|
98
|
+
src.session_id ?? null,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Record a member's provenance evolution row (best-effort, non-fatal). */
|
|
103
|
+
export function recordEvolution(
|
|
104
|
+
db: DatabaseSync,
|
|
105
|
+
topicId: string,
|
|
106
|
+
memoryId: string,
|
|
107
|
+
sessionId: string | null,
|
|
108
|
+
assignedAt: number,
|
|
109
|
+
method: "merge" | "split",
|
|
110
|
+
): void {
|
|
111
|
+
db.prepare(
|
|
112
|
+
`INSERT OR REPLACE INTO topic_evolution
|
|
113
|
+
(topic_id, memory_id, session_id, assigned_at, method)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
115
|
+
).run(topicId, memoryId, sessionId, assignedAt, method);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Create a new split topic, returning its id. */
|
|
119
|
+
export function createSplitTopic(
|
|
120
|
+
db: DatabaseSync,
|
|
121
|
+
sourceId: string,
|
|
122
|
+
sourceLabel: string,
|
|
123
|
+
now: number,
|
|
124
|
+
): string {
|
|
125
|
+
// Two splits in the same millisecond collide on the base id; append a
|
|
126
|
+
// numeric suffix until the id is unused.
|
|
127
|
+
let newId = `topic_${sourceId}_split_${now}`;
|
|
128
|
+
let suffix = 2;
|
|
129
|
+
let exists = db.prepare("SELECT 1 FROM topics WHERE id = ?").get(newId);
|
|
130
|
+
while (exists) {
|
|
131
|
+
newId = `topic_${sourceId}_split_${now}_${suffix}`;
|
|
132
|
+
exists = db.prepare("SELECT 1 FROM topics WHERE id = ?").get(newId);
|
|
133
|
+
suffix++;
|
|
134
|
+
}
|
|
135
|
+
db.prepare(
|
|
136
|
+
`INSERT INTO topics (id, label, term_scores, memory_count, last_updated, cluster_model_built_at)
|
|
137
|
+
VALUES (?, ?, NULL, 0, ?, NULL)`,
|
|
138
|
+
).run(newId, `${sourceLabel} (split)`, now);
|
|
139
|
+
return newId;
|
|
140
|
+
}
|