pi-mega-compact 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +3 -2
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
  3. package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
  4. package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
  5. package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
  6. package/dist/extensions/dashboard-server/routes-topics.js +87 -0
  7. package/dist/extensions/dashboard-server/routes-turns.js +251 -0
  8. package/dist/extensions/dashboard-server/routes.js +3 -1
  9. package/dist/extensions/dashboard-server/server.js +6 -2
  10. package/dist/extensions/mega-compact.js +10 -6
  11. package/dist/extensions/mega-config.js +4 -0
  12. package/dist/extensions/mega-events/agent-handlers.js +62 -49
  13. package/dist/extensions/mega-events/context-handler.js +48 -2
  14. package/dist/extensions/mega-events/session-handlers.js +24 -0
  15. package/dist/extensions/mega-metrics-cmds.js +90 -0
  16. package/dist/extensions/mega-pipeline/recall.js +47 -17
  17. package/dist/extensions/mega-topics-cmds.js +111 -0
  18. package/dist/extensions/mega-turn-store.js +110 -0
  19. package/dist/extensions/mega-turn-store.test.js +68 -0
  20. package/dist/src/config/turns.js +48 -0
  21. package/dist/src/fork.js +33 -0
  22. package/dist/src/fork.test.js +81 -0
  23. package/dist/src/intent.js +51 -0
  24. package/dist/src/intent.test.js +78 -0
  25. package/dist/src/metrics/index.js +4 -0
  26. package/dist/src/metrics/turns.js +118 -0
  27. package/dist/src/metrics/turns.test.js +152 -0
  28. package/dist/src/store/sqlite/raw-transcript.js +5 -3
  29. package/dist/src/store/sqlite.dbmirror.test.js +9 -0
  30. package/dist/src/store/turns/connection.js +123 -0
  31. package/dist/src/store/turns/contract-compliance.test.js +428 -0
  32. package/dist/src/store/turns/index.js +31 -0
  33. package/dist/src/store/turns/memory-store.js +357 -0
  34. package/dist/src/store/turns/memory-store.test.js +9 -0
  35. package/dist/src/store/turns/migrations.js +178 -0
  36. package/dist/src/store/turns/migrations.test.js +138 -0
  37. package/dist/src/store/turns/schema.js +115 -0
  38. package/dist/src/store/turns/sqlite-store.js +455 -0
  39. package/dist/src/store/turns/sqlite-store.test.js +144 -0
  40. package/dist/src/store/turns/types.js +16 -0
  41. package/dist/src/topics/cluster.js +141 -0
  42. package/dist/src/topics/cluster.test.js +150 -0
  43. package/dist/src/topics/index.js +6 -0
  44. package/dist/src/topics/kselection.js +142 -0
  45. package/dist/src/topics/labels.js +56 -0
  46. package/dist/src/topics/store.js +119 -0
  47. package/dist/src/topics/store.test.js +117 -0
  48. package/dist/src/topics/types.js +8 -0
  49. package/dist/src/wiki.js +133 -0
  50. package/dist/src/wiki.test.js +171 -0
  51. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DUulvXUY.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DUulvXUY.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-Dx0YtCNf.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-Dx0YtCNf.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DtkaUo1v.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DtkaUo1v.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-CLa4KfiZ.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-CLa4KfiZ.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-Q6tEIBBi.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-Q6tEIBBi.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Hq7GNWSY.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Hq7GNWSY.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-BtmBV6lU.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-BtmBV6lU.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-CL10PwVU.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-CL10PwVU.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DAwANwbI.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DAwANwbI.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-Ec08TZRQ.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-Ec08TZRQ.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-COb8dpTy.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-COb8dpTy.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js +2 -0
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js.map +1 -0
  75. package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js +2 -0
  76. package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js.map +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-UMrussFX.js} +10 -10
  78. package/extensions/dashboard-client/dist/assets/index-UMrussFX.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-BDHZ7CeS.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-BDHZ7CeS.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/index.html +2 -2
  83. package/extensions/dashboard-client/src/App.tsx +10 -2
  84. package/extensions/dashboard-client/src/api/client.ts +84 -1
  85. package/extensions/dashboard-client/src/main.tsx +1 -0
  86. package/extensions/dashboard-client/src/styles/turns.css +142 -0
  87. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +185 -0
  88. package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +326 -0
  89. package/extensions/dashboard-server/api-contracts/endpoints.ts +389 -291
  90. package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
  91. package/extensions/dashboard-server/api-contracts/index.ts +153 -133
  92. package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +920 -919
  94. package/extensions/dashboard-server/routes-topics.ts +119 -0
  95. package/extensions/dashboard-server/routes-turns.ts +282 -0
  96. package/extensions/dashboard-server/routes.ts +8 -1
  97. package/extensions/dashboard-server/server.ts +6 -2
  98. package/extensions/mega-compact.ts +75 -59
  99. package/extensions/mega-config.ts +232 -220
  100. package/extensions/mega-events/agent-handlers.ts +194 -151
  101. package/extensions/mega-events/context-handler.ts +50 -1
  102. package/extensions/mega-events/session-handlers.ts +23 -0
  103. package/extensions/mega-metrics-cmds.ts +114 -0
  104. package/extensions/mega-pipeline/recall.ts +215 -152
  105. package/extensions/mega-topics-cmds.ts +151 -0
  106. package/extensions/mega-turn-store.test.ts +108 -0
  107. package/extensions/mega-turn-store.ts +169 -0
  108. package/package.json +3 -1
  109. package/src/config/turns.ts +69 -0
  110. package/src/fork.test.ts +94 -0
  111. package/src/fork.ts +78 -0
  112. package/src/intent.test.ts +87 -0
  113. package/src/intent.ts +115 -0
  114. package/src/metrics/index.ts +9 -0
  115. package/src/metrics/turns.test.ts +165 -0
  116. package/src/metrics/turns.ts +201 -0
  117. package/src/store/sqlite/raw-transcript.ts +9 -3
  118. package/src/store/sqlite.dbmirror.test.ts +10 -0
  119. package/src/store/turns/connection.ts +137 -0
  120. package/src/store/turns/contract-compliance.test.ts +562 -0
  121. package/src/store/turns/index.ts +65 -0
  122. package/src/store/turns/memory-store.test.ts +15 -0
  123. package/src/store/turns/memory-store.ts +469 -0
  124. package/src/store/turns/migrations.test.ts +161 -0
  125. package/src/store/turns/migrations.ts +203 -0
  126. package/src/store/turns/schema.ts +158 -0
  127. package/src/store/turns/sqlite-store.test.ts +161 -0
  128. package/src/store/turns/sqlite-store.ts +656 -0
  129. package/src/store/turns/types.ts +181 -0
  130. package/src/topics/cluster.test.ts +176 -0
  131. package/src/topics/cluster.ts +191 -0
  132. package/src/topics/index.ts +18 -0
  133. package/src/topics/kselection.ts +182 -0
  134. package/src/topics/labels.ts +67 -0
  135. package/src/topics/store.test.ts +132 -0
  136. package/src/topics/store.ts +208 -0
  137. package/src/topics/types.ts +64 -0
  138. package/src/wiki.test.ts +217 -0
  139. package/src/wiki.ts +198 -0
  140. package/dist/extensions/openclaw-mega-compact.js +0 -291
  141. package/dist/src/minilm.js +0 -92
  142. package/dist/src/wordpiece.js +0 -129
  143. package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +0 -1
@@ -18,6 +18,8 @@ import {
18
18
  } from "../mega-pipeline.js";
19
19
  import { recallMemoriesAndInline } from "../../src/recall.js";
20
20
  import { vectorStats } from "../../src/vectorStore.js";
21
+ import { openTurnStore } from "../../src/store/turns/connection.js";
22
+ import { openIntentQueue } from "../../src/intent.js";
21
23
  import type { MegaConfig } from "../mega-config.js";
22
24
 
23
25
  /** Register session lifecycle event handlers. */
@@ -148,6 +150,27 @@ export function registerSessionHandlers(
148
150
  // ---- Auto-inline injection point: prepend staged recall to systemPrompt ----
149
151
  pi.on("before_agent_start", async (event, ctx) => {
150
152
  runtime.captureModel(ctx); // most reliable point ctx.model is populated
153
+ // S52A: consume any pending rewind intents the dashboard queued. The host
154
+ // polls the intent queue here (the only safe lifecycle point) and marks each
155
+ // consumed. Best-effort + non-fatal — the store never calls back into the
156
+ // host (ledger protocol). A real rewind (reloading pi's conversation to turn
157
+ // N) is host-specific; this consumes the intent so the round-trip completes.
158
+ try {
159
+ if (config.turnsDbEnabled) {
160
+ const tdb = openTurnStore(runtime.currentStateDir);
161
+ const q = openIntentQueue(tdb);
162
+ const pending = q.pendingIntents();
163
+ for (const intent of pending) {
164
+ q.consumeIntent(intent.id);
165
+ runtime.dashboard.event("rewind_intent_consumed", {
166
+ conversationId: intent.conversationId,
167
+ targetTurnIndex: intent.targetTurnIndex,
168
+ });
169
+ }
170
+ }
171
+ } catch {
172
+ /* non-fatal: intent polling never breaks the agent loop */
173
+ }
151
174
  const cpBlock = runtime.pendingRecallBlock;
152
175
  const memBlock = runtime.pendingMemoryRecallBlock;
153
176
  if (!cpBlock && !memBlock) return;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * mega-metrics-cmds.ts — S50C per-turn memory-quality /commands.
3
+ *
4
+ * Registers /mega-metrics and /mega-fork, backed by the pi-agnostic
5
+ * src/metrics/turns.ts rollups and src/fork.ts primitive over the isolated
6
+ * S49 turns.db. Read-only (PREVENT-PI-001/002 — these never mutate memory,
7
+ * drop ranges, or compaction); local SQLite only (PREVENT-PI-004);
8
+ * parameterized queries (PREVENT-002). Both require turnsDbEnabled (the
9
+ * isolated store); they no-op with guidance on the legacy main-db path.
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+ import type { MegaRuntime } from "./mega-runtime.js";
14
+ import type { MegaConfig } from "./mega-config.js";
15
+ import { turnStoreFor, mainDbFor } from "./mega-turn-store.js";
16
+ import { turnMetrics, conversationMetrics } from "../src/metrics/index.js";
17
+ import { forkFromConversation, ForkError } from "../src/fork.js";
18
+ import { ensureConversationIdFor } from "./mega-turn-store.js";
19
+
20
+ function pct(r: number): string {
21
+ return `${(r * 100).toFixed(1)}%`;
22
+ }
23
+
24
+ /** Register the /mega-metrics + /mega-fork commands. */
25
+ export function registerMetricsCommands(
26
+ pi: ExtensionAPI,
27
+ runtime: MegaRuntime,
28
+ config: MegaConfig,
29
+ ): void {
30
+ pi.registerCommand("mega-metrics", {
31
+ description:
32
+ "Show per-turn + per-conversation memory-quality metrics (recall reuse, dedup ratio, compression ratio). Usage: /mega-metrics [conversationId]",
33
+ handler: async (args: string, ctx: ExtensionContext) => {
34
+ try {
35
+ runtime.bindRepo(ctx.cwd);
36
+ const stateDir = runtime.currentStateDir;
37
+ const store = turnStoreFor(config, stateDir);
38
+ if (!store) {
39
+ ctx.ui.notify(
40
+ "[mega-compact] /mega-metrics requires the isolated turns.db (set turnsDbEnabled on).",
41
+ );
42
+ return;
43
+ }
44
+ const conv =
45
+ args.trim() ||
46
+ ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
47
+ const mainDb = mainDbFor(stateDir);
48
+ const perTurn = turnMetrics(store, mainDb, conv);
49
+ if (perTurn.length === 0) {
50
+ ctx.ui.notify(`[mega-compact] no turns recorded for conversation ${conv}.`);
51
+ return;
52
+ }
53
+ const agg = conversationMetrics(store, mainDb, conv);
54
+ ctx.ui.notify(
55
+ `[mega-compact] conversation ${conv} — ${agg.turnCount} turns, ${agg.epochCount} epoch(s), total recall ${agg.totalRecall}, total raw msgs ${agg.totalRawMessages}`,
56
+ );
57
+ ctx.ui.notify(
58
+ ` avg dedup unique ratio ${pct(agg.avgDedupUniqueRatio)} · avg compression ratio ${pct(agg.avgCompressionRatio)}`,
59
+ );
60
+ for (const t of perTurn) {
61
+ ctx.ui.notify(
62
+ ` turn ${t.turnIndex} (epoch ${t.epochId ?? "—"}): recall ${t.recallCount}, raw ${t.rawMessageCount}, dedup ${pct(t.dedupUniqueRatio)}, compress ${pct(t.compressionRatio)}`,
63
+ );
64
+ }
65
+ } catch (e) {
66
+ ctx.ui.notify(`[mega-compact] /mega-metrics failed: ${String(e)}`);
67
+ }
68
+ },
69
+ });
70
+
71
+ pi.registerCommand("mega-fork", {
72
+ description:
73
+ "Fork the current conversation at turn N into a new child conversation, rehydrating that turn's injected checkpoints (recall-to-point, not window replay). Usage: /mega-fork <turnIndex> [conversationId]",
74
+ handler: async (args: string, ctx: ExtensionContext) => {
75
+ try {
76
+ runtime.bindRepo(ctx.cwd);
77
+ const stateDir = runtime.currentStateDir;
78
+ const store = turnStoreFor(config, stateDir);
79
+ if (!store) {
80
+ ctx.ui.notify(
81
+ "[mega-compact] /mega-fork requires the isolated turns.db (set turnsDbEnabled on).",
82
+ );
83
+ return;
84
+ }
85
+ const parts = args.trim().split(/\s+/).filter(Boolean);
86
+ const turnIndex = Number.parseInt(parts[0] ?? "", 10);
87
+ if (!Number.isFinite(turnIndex)) {
88
+ ctx.ui.notify("[mega-compact] /mega-fork needs a turn index, e.g. /mega-fork 3");
89
+ return;
90
+ }
91
+ const conv =
92
+ parts[1] ||
93
+ ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
94
+ const out = forkFromConversation(store, conv, turnIndex);
95
+ runtime.logger.info("fork", {
96
+ parent: conv,
97
+ child: out.childConversationId,
98
+ turnIndex,
99
+ checkpoints: out.checkpointIds,
100
+ });
101
+ ctx.ui.notify(
102
+ `[mega-compact] forked ${conv} @ turn ${turnIndex} → ${out.childConversationId}\n` +
103
+ ` replay set (${out.checkpointIds.length} checkpoint(s)): ${out.checkpointIds.join(", ")}`,
104
+ );
105
+ } catch (e) {
106
+ if (e instanceof ForkError) {
107
+ ctx.ui.notify(`[mega-compact] /mega-fork: ${e.message}`);
108
+ } else {
109
+ ctx.ui.notify(`[mega-compact] /mega-fork failed: ${String(e)}`);
110
+ }
111
+ }
112
+ },
113
+ });
114
+ }
@@ -8,15 +8,25 @@
8
8
 
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
11
- import { recallAndInline, recallAndInlineAsync, formatRecallBlock, type RecallInjectResult } from "../../src/recall.js";
11
+ import {
12
+ recallAndInline,
13
+ recallAndInlineAsync,
14
+ formatRecallBlock,
15
+ type RecallInjectResult,
16
+ } from "../../src/recall.js";
12
17
  import { normalizeSessionId } from "../../src/store.js";
13
- import { incRecallInjected, incCacheHitTokens, getIndexDir } from "../../src/store/sqlite.js";
14
- import { ensureConversationId, recordTurn, recordTurnRecall, type RecallSource } from "../../src/store/sqlite/turns.js";
15
18
  import {
16
- type MegaRuntime,
17
- C,
18
- } from "../mega-runtime.js";
19
- import { type MegaConfig } from "../mega-config.js";
19
+ incRecallInjected,
20
+ incCacheHitTokens,
21
+ getIndexDir,
22
+ } from "../../src/store/sqlite.js";
23
+ import {
24
+ ensureConversationIdFor,
25
+ recordTurnWrite,
26
+ recordRecallWrite,
27
+ } from "../mega-turn-store.js";
28
+ import { type MegaRuntime, C } from "../mega-runtime.js";
29
+ import type { MegaConfig } from "../mega-config.js";
20
30
 
21
31
  /**
22
32
  * Unified recall (Layer 5). The ONE path that injects. Returns the recall
@@ -24,73 +34,101 @@ import { type MegaConfig } from "../mega-config.js";
24
34
  * or report it (command).
25
35
  */
26
36
  export function doRecall(
27
- runtime: MegaRuntime,
28
- config: MegaConfig,
29
- ctx: ExtensionContext,
30
- query: string,
31
- source: "resume" | "command",
37
+ runtime: MegaRuntime,
38
+ config: MegaConfig,
39
+ ctx: ExtensionContext,
40
+ query: string,
41
+ source: "resume" | "command",
32
42
  ) {
33
- runtime.bindRepo(ctx.cwd);
34
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
35
- // Live window text for inline dedupe (Fix C): drop recalled checkpoints that
36
- // are already resident in the session, so recall never re-injects context the
37
- // model can already see. Best-effort — an empty window just skips dedupe.
38
- const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
39
- const result = recallAndInline(
40
- {
41
- sessionId: sid,
42
- query,
43
- limit: config.autoInlineK,
44
- source,
45
- skipInjected: true,
46
- recallMaxTokens: config.recallMaxTokens,
47
- windowDedupe: config.windowDedupe,
48
- liveWindow,
49
- dedupSim: config.dedupSim,
50
- },
51
- runtime.store,
52
- );
53
- runtime.dashboard.event("recall", { source, query: query.slice(0, 120), injected: result.toInject.length, empty: result.empty });
54
- if (!result.empty && result.toInject.length > 0) {
55
- const top = result.toInject[0];
56
- const scorePct = Math.round((top.score ?? 0) * 100);
57
- const files = top.checkpoint.filesModified ?? [];
58
- const label = files.length ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ") : top.checkpoint.checkpointId;
59
- runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
60
- runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
61
- }
62
- if (result.toInject.length > 0) {
63
- let sumTokens = 0; for (const h of result.toInject) sumTokens += h.checkpoint.tokenEstimate;
64
- runtime.rt.recallInjections += result.toInject.length;
65
- runtime.rt.cacheHitTokens += sumTokens;
66
- incRecallInjected(result.toInject.length, runtime.currentStateDir);
67
- incCacheHitTokens(sumTokens, runtime.currentStateDir);
68
- // S43: record recall provenance — which checkpoints/summaries served this
69
- // turn, their score + source path. Linked to the turn row written at
70
- // turn_end via the conversation+turnIndex. Best-effort + non-fatal.
71
- try {
72
- const convId = ensureConversationId(sid, runtime.currentStateDir);
73
- const turnId = recordTurn({
74
- conversationId: convId,
75
- sessionId: sid,
76
- turnIndex: runtime.currentTurn,
77
- startedAt: Date.now(),
78
- }, runtime.currentStateDir);
79
- recordTurnRecall(
80
- turnId,
81
- result.toInject.map((h) => ({
82
- checkpointId: h.checkpoint.checkpointId,
83
- score: h.score,
84
- source: (h.raptorLevel !== undefined ? "raptor" : h.repoId ? "cross-repo" : "flat") as RecallSource,
85
- raptorLevel: h.raptorLevel,
86
- })),
87
- runtime.currentStateDir,
88
- );
89
- } catch {
90
- /* non-fatal: recall provenance never breaks the recall path */
91
- }
92
- }
93
- return result;
43
+ runtime.bindRepo(ctx.cwd);
44
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
45
+ // Live window text for inline dedupe (Fix C): drop recalled checkpoints that
46
+ // are already resident in the session, so recall never re-injects context the
47
+ // model can already see. Best-effort — an empty window just skips dedupe.
48
+ const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
49
+ const result = recallAndInline(
50
+ {
51
+ sessionId: sid,
52
+ query,
53
+ limit: config.autoInlineK,
54
+ source,
55
+ skipInjected: true,
56
+ recallMaxTokens: config.recallMaxTokens,
57
+ windowDedupe: config.windowDedupe,
58
+ liveWindow,
59
+ dedupSim: config.dedupSim,
60
+ },
61
+ runtime.store,
62
+ );
63
+ runtime.dashboard.event("recall", {
64
+ source,
65
+ query: query.slice(0, 120),
66
+ injected: result.toInject.length,
67
+ empty: result.empty,
68
+ });
69
+ if (!result.empty && result.toInject.length > 0) {
70
+ const top = result.toInject[0];
71
+ const scorePct = Math.round((top.score ?? 0) * 100);
72
+ const files = top.checkpoint.filesModified ?? [];
73
+ const label = files.length
74
+ ? files
75
+ .map((f) => f.split("/").pop() ?? f)
76
+ .slice(0, 2)
77
+ .join(", ")
78
+ : top.checkpoint.checkpointId;
79
+ runtime.pushTicker(
80
+ `${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`,
81
+ );
82
+ runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
83
+ }
84
+ if (result.toInject.length > 0) {
85
+ let sumTokens = 0;
86
+ for (const h of result.toInject) sumTokens += h.checkpoint.tokenEstimate;
87
+ runtime.rt.recallInjections += result.toInject.length;
88
+ runtime.rt.cacheHitTokens += sumTokens;
89
+ incRecallInjected(result.toInject.length, runtime.currentStateDir);
90
+ incCacheHitTokens(sumTokens, runtime.currentStateDir);
91
+ // S43: record recall provenance — which checkpoints/summaries served this
92
+ // turn, their score + source path. Linked to the turn row written at
93
+ // turn_end via the conversation+turnIndex. Best-effort + non-fatal.
94
+ try {
95
+ const convId = ensureConversationIdFor(
96
+ config,
97
+ sid,
98
+ runtime.currentStateDir,
99
+ );
100
+ const turnId = recordTurnWrite(
101
+ config,
102
+ {
103
+ conversationId: convId,
104
+ sessionId: sid,
105
+ turnIndex: runtime.currentTurn,
106
+ role: "assistant",
107
+ startedAt: Date.now(),
108
+ },
109
+ runtime.currentStateDir,
110
+ );
111
+ recordRecallWrite(
112
+ config,
113
+ turnId,
114
+ result.toInject.map((h) => ({
115
+ checkpointId: h.checkpoint.checkpointId,
116
+ score: h.score,
117
+ source:
118
+ h.raptorLevel !== undefined
119
+ ? "raptor"
120
+ : h.repoId
121
+ ? "cross-repo"
122
+ : "flat",
123
+ raptorLevel: h.raptorLevel,
124
+ })),
125
+ runtime.currentStateDir,
126
+ );
127
+ } catch {
128
+ /* non-fatal: recall provenance never breaks the recall path */
129
+ }
130
+ }
131
+ return result;
94
132
  }
95
133
 
96
134
  /**
@@ -105,72 +143,96 @@ export function doRecall(
105
143
  * the same-repo result unchanged.
106
144
  */
107
145
  export async function doRecallAsync(
108
- runtime: MegaRuntime,
109
- config: MegaConfig,
110
- ctx: ExtensionContext,
111
- query: string,
112
- source: "resume" | "command",
113
- opts: { crossRepo?: boolean } = {},
146
+ runtime: MegaRuntime,
147
+ config: MegaConfig,
148
+ ctx: ExtensionContext,
149
+ query: string,
150
+ source: "resume" | "command",
151
+ opts: { crossRepo?: boolean } = {},
114
152
  ): Promise<RecallInjectResult> {
115
- runtime.bindRepo(ctx.cwd);
116
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
117
- const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
118
- // Sync same-repo first (fast, never blocks).
119
- const sameRepo = recallAndInline(
120
- {
121
- sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
122
- recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
123
- liveWindow, dedupSim: config.dedupSim,
124
- },
125
- runtime.store,
126
- );
127
- if (!config.crossRepoEnabled || !opts.crossRepo) return sameRepo;
128
- if (sameRepo.toInject.length >= config.autoInlineK) return sameRepo; // same-repo satisfied
129
- // Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
130
- try {
131
- const x = await recallAndInlineAsync(
132
- {
133
- sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
134
- recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
135
- liveWindow, dedupSim: config.crossRepoCosine, crossRepo: true,
136
- // F2: resolve the machine-wide index dir via the shared resolver so the
137
- // cross-repo injected-set dedup works even when MEGACOMPACT_INDEX_DIR is
138
- // unset. The env var still wins when set (getIndexDir checks it first);
139
- // the default (~/.mega-compact-index) is the same DB mega-commands and the
140
- // dashboard read, so injection counts stay consistent. Without this, a
141
- // bare `process.env` read returns undefined → cross-repo hits re-inject in
142
- // every new session (the global injected-set is never consulted).
143
- globalIndexDir: getIndexDir(),
144
- },
145
- runtime.store,
146
- );
147
- runtime.dashboard.event("recall-crossrepo", {
148
- source, query: query.slice(0, 120), injected: x.toInject.length,
149
- sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
150
- });
151
- // Merge, dedup by checkpointId, respect the same token cap by reformatting.
152
- const seen = new Set(sameRepo.toInject.map((h) => h.checkpoint.checkpointId));
153
- const merged = [...sameRepo.toInject];
154
- for (const h of x.toInject) {
155
- if (!seen.has(h.checkpoint.checkpointId)) { merged.push(h); seen.add(h.checkpoint.checkpointId); }
156
- }
157
- const block = merged.length ? formatRecallBlock(merged) : "";
158
- if (merged.length > 0) {
159
- let sumTokens = 0; for (const h of merged) sumTokens += h.checkpoint.tokenEstimate;
160
- runtime.rt.recallInjections += merged.length;
161
- runtime.rt.cacheHitTokens += sumTokens;
162
- incRecallInjected(merged.length, runtime.currentStateDir);
163
- incCacheHitTokens(sumTokens, runtime.currentStateDir);
164
- }
165
- return {
166
- toInject: merged,
167
- report: merged.map((h) => ` • ${h.checkpoint.checkpointId}${h.repoId ? ` (from ${h.repoId.split("/").filter(Boolean).pop()})` : ""}`),
168
- block,
169
- empty: merged.length === 0,
170
- };
171
- } catch {
172
- return sameRepo; // cross-repo failure → same-repo only (non-fatal)
173
- }
153
+ runtime.bindRepo(ctx.cwd);
154
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
155
+ const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
156
+ // Sync same-repo first (fast, never blocks).
157
+ const sameRepo = recallAndInline(
158
+ {
159
+ sessionId: sid,
160
+ query,
161
+ limit: config.autoInlineK,
162
+ source,
163
+ skipInjected: true,
164
+ recallMaxTokens: config.recallMaxTokens,
165
+ windowDedupe: config.windowDedupe,
166
+ liveWindow,
167
+ dedupSim: config.dedupSim,
168
+ },
169
+ runtime.store,
170
+ );
171
+ if (!config.crossRepoEnabled || !opts.crossRepo) return sameRepo;
172
+ if (sameRepo.toInject.length >= config.autoInlineK) return sameRepo; // same-repo satisfied
173
+ // Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
174
+ try {
175
+ const x = await recallAndInlineAsync(
176
+ {
177
+ sessionId: sid,
178
+ query,
179
+ limit: config.autoInlineK,
180
+ source,
181
+ skipInjected: true,
182
+ recallMaxTokens: config.recallMaxTokens,
183
+ windowDedupe: config.windowDedupe,
184
+ liveWindow,
185
+ dedupSim: config.crossRepoCosine,
186
+ crossRepo: true,
187
+ // F2: resolve the machine-wide index dir via the shared resolver so the
188
+ // cross-repo injected-set dedup works even when MEGACOMPACT_INDEX_DIR is
189
+ // unset. The env var still wins when set (getIndexDir checks it first);
190
+ // the default (~/.mega-compact-index) is the same DB mega-commands and the
191
+ // dashboard read, so injection counts stay consistent. Without this, a
192
+ // bare `process.env` read returns undefined → cross-repo hits re-inject in
193
+ // every new session (the global injected-set is never consulted).
194
+ globalIndexDir: getIndexDir(),
195
+ },
196
+ runtime.store,
197
+ );
198
+ runtime.dashboard.event("recall-crossrepo", {
199
+ source,
200
+ query: query.slice(0, 120),
201
+ injected: x.toInject.length,
202
+ sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
203
+ });
204
+ // Merge, dedup by checkpointId, respect the same token cap by reformatting.
205
+ const seen = new Set(
206
+ sameRepo.toInject.map((h) => h.checkpoint.checkpointId),
207
+ );
208
+ const merged = [...sameRepo.toInject];
209
+ for (const h of x.toInject) {
210
+ if (!seen.has(h.checkpoint.checkpointId)) {
211
+ merged.push(h);
212
+ seen.add(h.checkpoint.checkpointId);
213
+ }
214
+ }
215
+ const block = merged.length ? formatRecallBlock(merged) : "";
216
+ if (merged.length > 0) {
217
+ let sumTokens = 0;
218
+ for (const h of merged) sumTokens += h.checkpoint.tokenEstimate;
219
+ runtime.rt.recallInjections += merged.length;
220
+ runtime.rt.cacheHitTokens += sumTokens;
221
+ incRecallInjected(merged.length, runtime.currentStateDir);
222
+ incCacheHitTokens(sumTokens, runtime.currentStateDir);
223
+ }
224
+ return {
225
+ toInject: merged,
226
+ report: merged.map(
227
+ (h) =>
228
+ ` • ${h.checkpoint.checkpointId}${h.repoId ? ` (from ${h.repoId.split("/").filter(Boolean).pop()})` : ""}`,
229
+ ),
230
+ block,
231
+ empty: merged.length === 0,
232
+ };
233
+ } catch {
234
+ return sameRepo; // cross-repo failure → same-repo only (non-fatal)
235
+ }
174
236
  }
175
237
 
176
238
  /**
@@ -180,18 +242,19 @@ export async function doRecallAsync(
180
242
  * Mirrors recentUserQuery's use of sessionEntryToContextMessages.
181
243
  */
182
244
  function extractLiveWindow(ctx: ExtensionContext): string[] {
183
- try {
184
- const entries = ctx.sessionManager.getEntries();
185
- const texts: string[] = [];
186
- for (const e of entries) {
187
- for (const m of sessionEntryToContextMessages(e)) {
188
- const c = (m as { content?: unknown }).content;
189
- if (typeof c === "string") texts.push(c);
190
- else if (Array.isArray(c)) texts.push(c.map((b: { text?: string }) => b.text ?? "").join(" "));
191
- }
192
- }
193
- return texts;
194
- } catch {
195
- return [];
196
- }
245
+ try {
246
+ const entries = ctx.sessionManager.getEntries();
247
+ const texts: string[] = [];
248
+ for (const e of entries) {
249
+ for (const m of sessionEntryToContextMessages(e)) {
250
+ const c = (m as { content?: unknown }).content;
251
+ if (typeof c === "string") texts.push(c);
252
+ else if (Array.isArray(c))
253
+ texts.push(c.map((b: { text?: string }) => b.text ?? "").join(" "));
254
+ }
255
+ }
256
+ return texts;
257
+ } catch {
258
+ return [];
259
+ }
197
260
  }