pi-mega-compact 0.7.8 → 0.8.0

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 (122) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/html.js +1023 -0
  3. package/dist/extensions/dashboard-server/html.test.js +41 -0
  4. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  5. package/dist/extensions/dashboard-server/server.js +530 -0
  6. package/dist/extensions/dashboard-server/server.test.js +120 -0
  7. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  8. package/dist/extensions/dashboard-server/state.js +30 -0
  9. package/dist/extensions/dashboard-server/types.js +5 -0
  10. package/dist/extensions/dashboard-server-s32.test.js +181 -0
  11. package/dist/extensions/dashboard-server.js +7 -1315
  12. package/dist/extensions/mega-commands.js +162 -134
  13. package/dist/extensions/mega-compact.js +3 -0
  14. package/dist/extensions/mega-compact.test.js +90 -21
  15. package/dist/extensions/mega-conflict-cmds.js +5 -1
  16. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  17. package/dist/extensions/mega-db-cmds.js +11 -2
  18. package/dist/extensions/mega-events/agent-handlers.js +222 -0
  19. package/dist/extensions/mega-events/compact-handlers.js +162 -0
  20. package/dist/extensions/mega-events/context-handler.js +249 -0
  21. package/dist/extensions/mega-events/register.js +21 -0
  22. package/dist/extensions/mega-events/session-handlers.js +142 -0
  23. package/dist/extensions/mega-events.js +15 -699
  24. package/dist/extensions/mega-game-cmds.js +106 -0
  25. package/dist/extensions/mega-game-cmds.test.js +113 -0
  26. package/dist/extensions/mega-pipeline/compact.js +324 -0
  27. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  28. package/dist/extensions/mega-pipeline/recall.js +147 -0
  29. package/dist/extensions/mega-pipeline.js +9 -480
  30. package/dist/extensions/mega-runtime/helpers.js +40 -0
  31. package/dist/extensions/mega-runtime/query.js +29 -0
  32. package/dist/extensions/mega-runtime/state.js +877 -0
  33. package/dist/extensions/mega-runtime/state.test.js +171 -0
  34. package/dist/extensions/mega-runtime/widget.js +270 -0
  35. package/dist/extensions/mega-runtime/widget.test.js +160 -0
  36. package/dist/extensions/mega-runtime.js +15 -947
  37. package/dist/src/config/themes.js +84 -0
  38. package/dist/src/config/themes.test.js +94 -0
  39. package/dist/src/game/scoring.js +105 -0
  40. package/dist/src/game/scoring.test.js +98 -0
  41. package/dist/src/store/sqlite/checkpoints.js +145 -0
  42. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  43. package/dist/src/store/sqlite/foundation.js +38 -0
  44. package/dist/src/store/sqlite/game-achievements.js +111 -0
  45. package/dist/src/store/sqlite/game-achievements.test.js +67 -0
  46. package/dist/src/store/sqlite/game-scores.js +105 -0
  47. package/dist/src/store/sqlite/game-scores.test.js +106 -0
  48. package/dist/src/store/sqlite/game-state.js +54 -0
  49. package/dist/src/store/sqlite/game-state.test.js +76 -0
  50. package/dist/src/store/sqlite/global-index.js +224 -0
  51. package/dist/src/store/sqlite/maintenance.js +235 -0
  52. package/dist/src/store/sqlite/memories.js +164 -0
  53. package/dist/src/store/sqlite/meta.js +82 -0
  54. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  55. package/dist/src/store/sqlite/raptor.js +57 -0
  56. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  57. package/dist/src/store/sqlite/schema.js +294 -0
  58. package/dist/src/store/sqlite/session-state.js +28 -0
  59. package/dist/src/store/sqlite/stats.js +66 -0
  60. package/dist/src/store/sqlite/utils.js +120 -0
  61. package/dist/src/store/sqlite.js +23 -1607
  62. package/extensions/dashboard-server/html.test.ts +50 -0
  63. package/extensions/dashboard-server/html.ts +1026 -0
  64. package/extensions/dashboard-server/index-reader.ts +130 -0
  65. package/extensions/dashboard-server/server.test.ts +131 -0
  66. package/extensions/dashboard-server/server.ts +505 -0
  67. package/extensions/dashboard-server/snapshot.ts +44 -0
  68. package/extensions/dashboard-server/state.ts +33 -0
  69. package/extensions/dashboard-server/types.ts +134 -0
  70. package/extensions/dashboard-server-s32.test.ts +195 -0
  71. package/extensions/dashboard-server.ts +7 -1431
  72. package/extensions/mega-commands.ts +33 -10
  73. package/extensions/mega-compact.test.ts +198 -43
  74. package/extensions/mega-compact.ts +3 -0
  75. package/extensions/mega-conflict-cmds.ts +6 -2
  76. package/extensions/mega-dashboard-cmds.ts +30 -23
  77. package/extensions/mega-db-cmds.ts +11 -3
  78. package/extensions/mega-events/agent-handlers.ts +262 -0
  79. package/extensions/mega-events/compact-handlers.ts +192 -0
  80. package/extensions/mega-events/context-handler.ts +290 -0
  81. package/extensions/mega-events/register.ts +37 -0
  82. package/extensions/mega-events/session-handlers.ts +165 -0
  83. package/extensions/mega-events.ts +15 -780
  84. package/extensions/mega-game-cmds.test.ts +137 -0
  85. package/extensions/mega-game-cmds.ts +122 -0
  86. package/extensions/mega-pipeline/compact.ts +366 -0
  87. package/extensions/mega-pipeline/memory-review.ts +46 -0
  88. package/extensions/mega-pipeline/recall.ts +165 -0
  89. package/extensions/mega-pipeline.ts +9 -537
  90. package/extensions/mega-runtime/helpers.ts +68 -0
  91. package/extensions/mega-runtime/query.ts +29 -0
  92. package/extensions/mega-runtime/state.test.ts +171 -0
  93. package/extensions/mega-runtime/state.ts +967 -0
  94. package/extensions/mega-runtime/widget.test.ts +185 -0
  95. package/extensions/mega-runtime/widget.ts +359 -0
  96. package/extensions/mega-runtime.ts +15 -1093
  97. package/package.json +4 -3
  98. package/src/config/themes.test.ts +116 -0
  99. package/src/config/themes.ts +124 -0
  100. package/src/game/scoring.test.ts +103 -0
  101. package/src/game/scoring.ts +158 -0
  102. package/src/store/sqlite/checkpoints.ts +204 -0
  103. package/src/store/sqlite/dedup-mirror.ts +114 -0
  104. package/src/store/sqlite/foundation.ts +63 -0
  105. package/src/store/sqlite/game-achievements.test.ts +80 -0
  106. package/src/store/sqlite/game-achievements.ts +147 -0
  107. package/src/store/sqlite/game-scores.test.ts +132 -0
  108. package/src/store/sqlite/game-scores.ts +168 -0
  109. package/src/store/sqlite/game-state.test.ts +89 -0
  110. package/src/store/sqlite/game-state.ts +87 -0
  111. package/src/store/sqlite/global-index.ts +305 -0
  112. package/src/store/sqlite/maintenance.ts +294 -0
  113. package/src/store/sqlite/memories.ts +217 -0
  114. package/src/store/sqlite/meta.ts +108 -0
  115. package/src/store/sqlite/model-snapshots.ts +83 -0
  116. package/src/store/sqlite/raptor.ts +107 -0
  117. package/src/store/sqlite/raw-transcript.ts +221 -0
  118. package/src/store/sqlite/schema.ts +305 -0
  119. package/src/store/sqlite/session-state.ts +38 -0
  120. package/src/store/sqlite/stats.ts +127 -0
  121. package/src/store/sqlite/utils.ts +125 -0
  122. package/src/store/sqlite.ts +23 -2204
@@ -0,0 +1,147 @@
1
+ /**
2
+ * recall.ts — unified Layer-5 recall pipeline.
3
+ *
4
+ * `doRecall` is the ONE path that injects (sync). `doRecallAsync` augments with
5
+ * optional cross-repo HNSW on resume / /mega-recall --cross-repo. Both mutate
6
+ * the shared MegaRuntime (token accounting, ticker, dashboard events).
7
+ */
8
+ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
9
+ import { recallAndInline, recallAndInlineAsync, formatRecallBlock } from "../../src/recall.js";
10
+ import { normalizeSessionId } from "../../src/store.js";
11
+ import { incRecallInjected, incCacheHitTokens } from "../../src/store/sqlite.js";
12
+ import { C, } from "../mega-runtime.js";
13
+ /**
14
+ * Unified recall (Layer 5). The ONE path that injects. Returns the recall
15
+ * result; callers decide whether to stage it for before_agent_start (resume)
16
+ * or report it (command).
17
+ */
18
+ export function doRecall(runtime, config, ctx, query, source) {
19
+ runtime.bindRepo(ctx.cwd);
20
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
21
+ // Live window text for inline dedupe (Fix C): drop recalled checkpoints that
22
+ // are already resident in the session, so recall never re-injects context the
23
+ // model can already see. Best-effort — an empty window just skips dedupe.
24
+ const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
25
+ const result = recallAndInline({
26
+ sessionId: sid,
27
+ query,
28
+ limit: config.autoInlineK,
29
+ source,
30
+ skipInjected: true,
31
+ recallMaxTokens: config.recallMaxTokens,
32
+ windowDedupe: config.windowDedupe,
33
+ liveWindow,
34
+ dedupSim: config.dedupSim,
35
+ }, runtime.store);
36
+ runtime.dashboard.event("recall", { source, query: query.slice(0, 120), injected: result.toInject.length, empty: result.empty });
37
+ if (!result.empty && result.toInject.length > 0) {
38
+ const top = result.toInject[0];
39
+ const scorePct = Math.round((top.score ?? 0) * 100);
40
+ const files = top.checkpoint.filesModified ?? [];
41
+ const label = files.length ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ") : top.checkpoint.checkpointId;
42
+ runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
43
+ runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
44
+ }
45
+ if (result.toInject.length > 0) {
46
+ let sumTokens = 0;
47
+ for (const h of result.toInject)
48
+ sumTokens += h.checkpoint.tokenEstimate;
49
+ runtime.rt.recallInjections += result.toInject.length;
50
+ runtime.rt.cacheHitTokens += sumTokens;
51
+ incRecallInjected(result.toInject.length, runtime.currentStateDir);
52
+ incCacheHitTokens(sumTokens, runtime.currentStateDir);
53
+ }
54
+ return result;
55
+ }
56
+ /**
57
+ * S17: async recall with optional cross-repo augmentation. Used on resume
58
+ * (session_start) and /mega-recall --cross-repo — NEVER from the mid-turn
59
+ * context handler (that stays sync). Runs the sync same-repo scan first; if it
60
+ * returns < config.autoInlineK hits AND crossRepo is enabled, awaits the PGlite
61
+ * HNSW cross-repo path and merges (source-labeled, deduped by checkpointId). The
62
+ * recallMaxTokens cap + windowDedupe apply to the merged set so cross-repo can
63
+ * never net-inflate the window. Cross-repo uses a stricter cosine floor
64
+ * (config.crossRepoCosine) than same-repo. Non-fatal: any async failure returns
65
+ * the same-repo result unchanged.
66
+ */
67
+ export async function doRecallAsync(runtime, config, ctx, query, source, opts = {}) {
68
+ runtime.bindRepo(ctx.cwd);
69
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
70
+ const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
71
+ // Sync same-repo first (fast, never blocks).
72
+ const sameRepo = recallAndInline({
73
+ sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
74
+ recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
75
+ liveWindow, dedupSim: config.dedupSim,
76
+ }, runtime.store);
77
+ if (!config.crossRepoEnabled || !opts.crossRepo)
78
+ return sameRepo;
79
+ if (sameRepo.toInject.length >= config.autoInlineK)
80
+ return sameRepo; // same-repo satisfied
81
+ // Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
82
+ try {
83
+ const x = await recallAndInlineAsync({
84
+ sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
85
+ recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
86
+ liveWindow, dedupSim: config.crossRepoCosine, crossRepo: true,
87
+ globalIndexDir: process.env.MEGACOMPACT_INDEX_DIR,
88
+ }, runtime.store);
89
+ runtime.dashboard.event("recall-crossrepo", {
90
+ source, query: query.slice(0, 120), injected: x.toInject.length,
91
+ sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
92
+ });
93
+ // Merge, dedup by checkpointId, respect the same token cap by reformatting.
94
+ const seen = new Set(sameRepo.toInject.map((h) => h.checkpoint.checkpointId));
95
+ const merged = [...sameRepo.toInject];
96
+ for (const h of x.toInject) {
97
+ if (!seen.has(h.checkpoint.checkpointId)) {
98
+ merged.push(h);
99
+ seen.add(h.checkpoint.checkpointId);
100
+ }
101
+ }
102
+ const block = merged.length ? formatRecallBlock(merged) : "";
103
+ if (merged.length > 0) {
104
+ let sumTokens = 0;
105
+ for (const h of merged)
106
+ sumTokens += h.checkpoint.tokenEstimate;
107
+ runtime.rt.recallInjections += merged.length;
108
+ runtime.rt.cacheHitTokens += sumTokens;
109
+ incRecallInjected(merged.length, runtime.currentStateDir);
110
+ incCacheHitTokens(sumTokens, runtime.currentStateDir);
111
+ }
112
+ return {
113
+ toInject: merged,
114
+ report: merged.map((h) => ` • ${h.checkpoint.checkpointId}${h.repoId ? ` (from ${h.repoId.split("/").filter(Boolean).pop()})` : ""}`),
115
+ block,
116
+ empty: merged.length === 0,
117
+ };
118
+ }
119
+ catch {
120
+ return sameRepo; // cross-repo failure → same-repo only (non-fatal)
121
+ }
122
+ }
123
+ /**
124
+ * Extract the live-window message texts from the session manager (Fix C),
125
+ * for inline-dedupe of recalled checkpoints. Best-effort: returns [] on any
126
+ * error so recall falls back to unbounded (still correct, just no dedupe).
127
+ * Mirrors recentUserQuery's use of sessionEntryToContextMessages.
128
+ */
129
+ function extractLiveWindow(ctx) {
130
+ try {
131
+ const entries = ctx.sessionManager.getEntries();
132
+ const texts = [];
133
+ for (const e of entries) {
134
+ for (const m of sessionEntryToContextMessages(e)) {
135
+ const c = m.content;
136
+ if (typeof c === "string")
137
+ texts.push(c);
138
+ else if (Array.isArray(c))
139
+ texts.push(c.map((b) => b.text ?? "").join(" "));
140
+ }
141
+ }
142
+ return texts;
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }
@@ -1,487 +1,16 @@
1
1
  /**
2
- * mega-pipeline.ts — the compaction + recall pipelines.
2
+ * mega-pipeline.ts — barrel re-export of the compaction + recall pipelines.
3
+ *
4
+ * Split into focused submodules under `./mega-pipeline/`:
5
+ * - memory-review.ts — `runMemoryReview`
6
+ * - compact.ts — `runCompact`, `piCompactWouldNoop`, `RunCompactResult`
7
+ * - recall.ts — `doRecall`, `doRecallAsync`
3
8
  *
4
9
  * `runCompact` runs the full Trident pipeline (fast-gate aside) and persists a
5
10
  * checkpoint. `doRecall` is the unified Layer-5 recall entry point. Both mutate
6
11
  * the shared MegaRuntime (token accounting, ticker, status, events) and are
7
12
  * driven by the event + command handlers in mega-events.ts / mega-commands.ts.
8
13
  */
9
- import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
10
- import { compactSession } from "../src/engine.js";
11
- import { recallAndInline, recallAndInlineAsync, formatRecallBlock } from "../src/recall.js";
12
- import { normalizeSessionId } from "../src/store.js";
13
- import { estimateBlockTokens } from "../src/tokens.js";
14
- import { touchSession, logDaily, incCompactCount, incRecallInjected, incCacheHitTokens } from "../src/store/sqlite.js";
15
- import { consolidateMemories } from "../src/memory.js";
16
- import { C, MARKER_TYPE, } from "./mega-runtime.js";
17
- import { resolveRepoRoot, preserveRecentForPressure } from "./mega-config.js";
18
- import { runRaptor } from "../src/dedup/raptor/index.js";
19
- import { loadDedupConfig } from "../src/config/dedup.js";
20
- import { upsertEmbedding as indexUpsertEmbedding } from "../src/store/vectorIndex.js";
21
- /**
22
- * Review the live conversation and persist durable memories (S20+S24). Shared by
23
- * the pressure-scaled turn-end cadence (mega-events.ts) AND review-on-compact
24
- * (below) so both paths run the identical review body. Best-effort + non-fatal:
25
- * a review failure is swallowed and never breaks the caller. On success, the
26
- * number of applied ops is returned so callers can feed the consolidation gate.
27
- *
28
- * @param view the engine message view to review (caller builds it)
29
- * @param label a short source tag for the ticker line (e.g. "pressure" / "turn")
30
- */
31
- export async function runMemoryReview(runtime, view, label) {
32
- try {
33
- const { reviewConversation } = await import("../src/memory.js");
34
- const { applyMemoryOps } = await import("../src/memoryOps.js");
35
- const ops = reviewConversation(view, []);
36
- if (ops.length) {
37
- await applyMemoryOps(ops, runtime.currentStateDir);
38
- // S21.2: ops landed — the compaction path reads this counter and fires
39
- // `consolidateMemories` only when > 0.
40
- runtime.memoriesTouchedThisCompaction += ops.length;
41
- runtime.pushTicker(`${C.green}🧠${C.reset} reviewed ${ops.length} memory op${ops.length === 1 ? "" : "s"} (${label})`);
42
- }
43
- return ops.length;
44
- }
45
- catch {
46
- /* non-fatal — auto-review must never break the turn loop / compaction */
47
- return 0;
48
- }
49
- }
50
- /** Run the full compaction pipeline and persist a checkpoint. Returns the result. */
51
- export function runCompact(pi, runtime, config, ctx, messages, opts = {}) {
52
- runtime.bindRepo(ctx.cwd);
53
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
54
- runtime.resetRuntime(sid);
55
- runtime.rt.sessionId = sid;
56
- const view = runtime.engineView(messages);
57
- // keepFrom deepens with context pressure (Fix E): under high pressure we
58
- // compact more of the session, down to the preserveRecentMin floor.
59
- const preserve = preserveRecentForPressure(opts.compressionPressure ?? 0, config.preserveRecent, config.preserveRecentMin);
60
- const keepFrom = opts.keepFrom ?? Math.max(0, view.length - preserve);
61
- // For very small sessions (fewer messages than preserveRecent), allow
62
- // compacting everything except the last message — the user explicitly
63
- // requested compaction, so don't refuse it just because the session is short.
64
- if (keepFrom <= 0) {
65
- if (view.length <= 1)
66
- return { skipped: true };
67
- // Use the fallback: compact everything except the last message
68
- const fallbackKeepFrom = view.length - 1;
69
- return doCompact(view, fallbackKeepFrom, opts, sid, config, pi, ctx, runtime);
70
- }
71
- return doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime);
72
- }
73
- function doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime) {
74
- runtime.pulsing = true; // animate the status line while the (sync) pipeline runs
75
- // S21.2: reset the per-compaction memory-op counter so the post-compact
76
- // consolidate pass only fires when memory rows actually changed during the
77
- // compaction window (turn_end → auto-review may have written some).
78
- runtime.memoriesTouchedThisCompaction = 0;
79
- const result = compactSession({
80
- sessionId: sid,
81
- messages: view,
82
- keepFrom,
83
- summary: opts.summary,
84
- timestamp: Date.now(),
85
- onTier: runtime.makeTierCallback(ctx),
86
- compressionPressure: opts.compressionPressure,
87
- }, runtime.store);
88
- runtime.pulsing = false;
89
- if (result.skipped)
90
- return { skipped: true };
91
- if (!result.deduped) {
92
- runtime.rt.persistedThisSession = true;
93
- runtime.rt.lastCheckpointId = result.checkpointId;
94
- }
95
- runtime.rt.lastCompactedFrom = result.compactedFrom;
96
- runtime.rt.lastCompactedTokens = result.tokenEstimate;
97
- runtime.rt.dedupAttempts++;
98
- // Honest "tokens saved" for this session-instance only:
99
- // new checkpoint → original − stored
100
- // deduped onto existing → whole original region (nothing new stored)
101
- // Resets to 0 on session_start (rt is rebuilt) — so a fresh session shows 0
102
- // while the repo's cumulative saved (SQLite meta) keeps the running total.
103
- const saved = result.deduped
104
- ? result.originalTokenEstimate
105
- : Math.max(0, result.originalTokenEstimate - result.tokenEstimate);
106
- runtime.rt.tokensSaved += saved;
107
- runtime.rt.compactCount += 1;
108
- incCompactCount(runtime.currentStateDir);
109
- if (result.deduped) {
110
- runtime.rt.cacheHitTokens += saved;
111
- incCacheHitTokens(saved, runtime.currentStateDir);
112
- }
113
- runtime.rt.lastCompactAt = Date.now();
114
- if (result.deduped)
115
- runtime.rt.dedupSkips++;
116
- // Grow the rolling "saved" goal so the progress bar always has a fresh
117
- // denominator (we don't want it pinned at 100% once we pass an old target).
118
- if (runtime.rt.tokensSaved > runtime.savedGoal)
119
- runtime.savedGoal = Math.ceil((runtime.rt.tokensSaved * 1.25) / 10_000) * 10_000;
120
- // Live toolbar activity: what file/region just got compacted or deduped.
121
- // Rendered via the rotating ticker line (see snapshot); the ring buffer is
122
- // cycled one-per-repaint so the single line scrolls through recent files.
123
- const files = result.filesModified ?? [];
124
- const fileLabel = files.length
125
- ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ")
126
- : result.regionHash.slice(0, 8);
127
- runtime.lastActivityAt = Date.now();
128
- // Explain-why line: surfaced while fresh. Pulls the dedup reason (which for
129
- // L2 includes the cosine sim) so the user sees WHY a region was kept/dropped.
130
- runtime.lastWhy = result.deduped
131
- ? `why: deduped@${result.dedupReason ?? "tier"}`
132
- : `why: compacted → ${result.checkpointId}`;
133
- // Recall/activity ticker: record this event in the ring buffer.
134
- const savedK = (saved / 1000).toFixed(1);
135
- runtime.pushTicker(result.deduped
136
- ? `${C.green}♻${C.reset} deduped ${fileLabel} · ${savedK}k saved`
137
- : `${C.cyan}🗜${C.reset} ${result.checkpointId} · +${savedK}k · ${fileLabel}`);
138
- // The per-tier trace has settled into the final outcome — fold it back into
139
- // the activity line and stop showing the live trace.
140
- runtime.tierTrace = undefined;
141
- // Record session activity + a daily-log entry in the per-repo SQLite store
142
- // (foundation for resume-sessions / daily-log features). Best-effort — never
143
- // block a compaction on bookkeeping.
144
- try {
145
- const root = resolveRepoRoot(ctx.cwd);
146
- touchSession(sid, root, runtime.currentStateDir);
147
- logDaily(sid, "compact", result.checkpointId, saved, runtime.currentStateDir);
148
- }
149
- catch {
150
- /* non-fatal: stats bookkeeping only */
151
- }
152
- // S21.2: best-effort consolidation of near-duplicate memories for this repo.
153
- // Runs after the per-repo stats touch so `consolidateMemories` can use the
154
- // same stateDir. Non-fatal — a failed consolidate never blocks a compaction.
155
- // Only runs when new memory ops landed in this pass (otherwise the prior
156
- // compaction's consolidate already had its shot — re-running would just
157
- // touch every row again with no merges).
158
- if (!result.deduped && runtime.memoriesTouchedThisCompaction > 0) {
159
- try {
160
- const root = resolveRepoRoot(ctx.cwd);
161
- void consolidateMemories(runtime.currentStateDir, root).then((n) => {
162
- if (n > 0)
163
- runtime.pushTicker(`${C.green}∫${C.reset} consolidated ${n} memory dup${n === 1 ? "" : "s"}`);
164
- }, () => {
165
- /* swallow: consolidate failures must never surface to the user */
166
- });
167
- }
168
- catch {
169
- /* non-fatal */
170
- }
171
- }
172
- // S24 review-on-compact: when pressure is high, the just-compacted region is
173
- // exactly the context worth remembering, so review it immediately rather than
174
- // waiting for the next turn-cadence tick. Uses the shared runMemoryReview
175
- // helper (fire-and-forget; doCompact is sync). Best-effort + non-fatal. Only
176
- // fires above the `high` band so low-pressure compactions don't pay the cost.
177
- if (!result.deduped && config.memoryAutoReview && runtime.pressureBand !== "low" && runtime.pressureBand !== "medium") {
178
- void runMemoryReview(runtime, view, "pressure");
179
- }
180
- // Sentinel marker: a non-LLM bookkeeping entry so subsequent triggers can
181
- // skip re-vectorizing an already-compacted region (zero token cost).
182
- pi.appendEntry(MARKER_TYPE, {
183
- checkpointId: result.checkpointId,
184
- regionHash: result.regionHash,
185
- tokenEstimate: result.tokenEstimate,
186
- deduped: result.deduped,
187
- });
188
- // Fix D: refresh the RAPTOR tree for this session so live recall (search) can
189
- // serve high-level summaries. Best-effort + non-fatal: never block compaction.
190
- // Budget-guarded (RAPTOR_BUDGET_MS) so it can't hang a large session.
191
- if (config.raptorEnabled && !result.deduped) {
192
- try {
193
- const dd = loadDedupConfig();
194
- const all = runtime.store.list(sid);
195
- const leaves = all.map((cp) => ({
196
- id: cp.checkpointId,
197
- messages: [],
198
- sourceText: cp.normalizedText ?? cp.summary ?? cp.regionHash,
199
- embedding: cp.embedding,
200
- }));
201
- if (leaves.length >= 2) {
202
- // S25: stamp the tree with the newest checkpoint epoch so the
203
- // freshness guard in raptorSearchHits can reject stale trees after a
204
- // later compaction adds newer checkpoints.
205
- const builtAt = all.length > 0 ? Math.max(...all.map((c) => c.timestamp)) : Date.now();
206
- runRaptor(leaves, {
207
- stateDir: runtime.currentStateDir,
208
- sessionId: sid,
209
- budgetMs: dd.RAPTOR_BUDGET_MS,
210
- clustersPerLevel: dd.RAPTOR_CLUSTERS_PER_LEVEL,
211
- consistencyThreshold: dd.RAPTOR_CONSISTENCY,
212
- logger: runtime.logger,
213
- builtAt: Number.isFinite(builtAt) ? builtAt : Date.now(),
214
- });
215
- }
216
- }
217
- catch {
218
- /* non-fatal: tree refresh never blocks a compaction */
219
- }
220
- }
221
- // Slice 2: best-effort mirror of the new checkpoint into the async global
222
- // PGlite/HNSW vector index. Fires once per compaction (not per-add), so the
223
- // shared global dir is never hammered by concurrent test workers.
224
- // Non-fatal: a WASM init failure degrades to the sync scan silently.
225
- if (!result.deduped) {
226
- try {
227
- const all = runtime.store.list(sid);
228
- const latest = all.find((cp) => cp.checkpointId === result.checkpointId);
229
- if (latest?.embedding) {
230
- void indexUpsertEmbedding(runtime.currentStateDir, sid, latest.checkpointId, latest.embedding).catch(() => {
231
- /* non-fatal: index refresh never blocks a compaction */
232
- });
233
- }
234
- }
235
- catch {
236
- /* non-fatal: index refresh never blocks a compaction */
237
- }
238
- }
239
- runtime.setStatus(ctx, runtime.rt.persistedThisSession
240
- ? `mega-compact: ${result.checkpointId} · ${saved} tok saved`
241
- : `mega-compact: ready`);
242
- runtime.logger.info("compact", {
243
- sessionId: sid,
244
- checkpointId: result.checkpointId ?? "(deduped)",
245
- deduped: result.deduped,
246
- tokenEstimate: saved,
247
- compactedFrom: result.compactedFrom,
248
- });
249
- runtime.dashboard.event("compact", {
250
- sessionId: sid,
251
- checkpointId: result.checkpointId ?? "(deduped)",
252
- deduped: result.deduped,
253
- tokenEstimate: saved,
254
- compactedFrom: result.compactedFrom,
255
- });
256
- runtime.snapshot(ctx);
257
- return { skipped: false, result, keepFrom, saved };
258
- }
259
- /**
260
- * Predict whether pi's `ctx.compact()` would throw a no-op error — "Already
261
- * compacted" or "Nothing to compact (session too small)" — so the auto-trigger
262
- * can SKIP the call instead of surfacing a hard, user-facing error.
263
- *
264
- * Why we can't intercept or suppress it: pi's public `compact()` computes
265
- * `prepareCompaction()` and throws *before* it emits `session_before_compact`,
266
- * so our handler there never runs on the no-op path. And `ctx.compact()`'s
267
- * `onError` callback fires only AFTER pi has already emitted a `compaction_end`
268
- * event carrying the error message (which the interactive UI renders) — so
269
- * `onError` cannot mute it either. The only robust fix is to not call
270
- * `ctx.compact()` when pi would no-op. (pi's own `_runAutoCompaction` path is
271
- * silent on this same condition; the public path we're forced through is the
272
- * one that throws.)
273
- *
274
- * Skipping is correct, not a compromise: by the time this runs, `runCompact()`
275
- * has already persisted the recall checkpoint (Path A). The durable on-disk
276
- * trim is only useful when pi can actually summarize a region; a transcript
277
- * under pi's `keepRecentTokens` budget is small enough that reloading it on
278
- * resume isn't a token-growth problem, so the durable trim is unnecessary
279
- * there anyway.
280
- *
281
- * Mirrors pi's `prepareCompaction()` return-undefined conditions (compaction.js):
282
- * (1) last entry is a compaction → "Already compacted"
283
- * (2) <2 cut-point messages since the last compaction → nothing to summarize
284
- * (a cut point = any non-toolResult message — user/assistant/bash/custom/
285
- * branchSummary/compactionSummary — matching pi's isCutPointMessage)
286
- * (3) transcript tokens since the last compaction < keepRecentTokens → pi
287
- * keeps everything → nothing to summarize
288
- * `keepRecentTokens` isn't readable from the extension API, so (3) uses the pi
289
- * default (20000) as a conservative floor; raise it via
290
- * `MEGACOMPACT_DURABLE_TRIM_FLOOR` if you raise pi's `compact.keepRecentTokens`.
291
- *
292
- * Best-effort: on any read error returns true (skip) — skipping a durable trim
293
- * is always safe; calling `ctx.compact()` on a no-op throws to the user.
294
- */
295
- export function piCompactWouldNoop(ctx) {
296
- try {
297
- const branch = ctx.sessionManager.getBranch();
298
- if (branch.length === 0)
299
- return true;
300
- // (1) already compacted — pi throws "Already compacted"
301
- if (branch[branch.length - 1].type === "compaction")
302
- return true;
303
- // boundaryStart = index just after the most recent compaction entry (or 0)
304
- let boundaryStart = 0;
305
- for (let i = branch.length - 1; i >= 0; i--) {
306
- if (branch[i].type === "compaction") {
307
- boundaryStart = i + 1;
308
- break;
309
- }
310
- }
311
- let cutPoints = 0;
312
- let tokens = 0;
313
- for (let i = boundaryStart; i < branch.length; i++) {
314
- const e = branch[i];
315
- if (e.type === "compaction")
316
- continue;
317
- let isCut = false;
318
- for (const m of sessionEntryToContextMessages(e)) {
319
- // pi's isCutPointMessage: every role except toolResult
320
- if (m.role !== "toolResult")
321
- isCut = true;
322
- const c = m.content;
323
- const text = typeof c === "string" ? c
324
- : Array.isArray(c)
325
- ? c.map((b) => b?.text ?? "").join(" ")
326
- : "";
327
- if (text)
328
- tokens += estimateBlockTokens(text);
329
- }
330
- if (isCut)
331
- cutPoints++;
332
- }
333
- // (2) need >=2 cut points so the kept cut isn't the first message
334
- if (cutPoints < 2)
335
- return true;
336
- // (3) transcript under pi's keepRecentTokens budget → pi keeps everything
337
- if (tokens < durableTrimFloorTokens())
338
- return true;
339
- return false;
340
- }
341
- catch {
342
- return true; // safe: skip the durable trim rather than risk a user-facing throw
343
- }
344
- }
345
- /** pi's default keepRecentTokens (compaction settings). Override with
346
- * MEGACOMPACT_DURABLE_TRIM_FLOOR if you raise pi's compact.keepRecentTokens. */
347
- function durableTrimFloorTokens() {
348
- const raw = process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
349
- if (raw !== undefined && Number.isFinite(Number(raw)))
350
- return Number(raw);
351
- return 20_000;
352
- }
353
- /**
354
- * Unified recall (Layer 5). The ONE path that injects. Returns the recall
355
- * result; callers decide whether to stage it for before_agent_start (resume)
356
- * or report it (command).
357
- */
358
- export function doRecall(runtime, config, ctx, query, source) {
359
- runtime.bindRepo(ctx.cwd);
360
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
361
- // Live window text for inline dedupe (Fix C): drop recalled checkpoints that
362
- // are already resident in the session, so recall never re-injects context the
363
- // model can already see. Best-effort — an empty window just skips dedupe.
364
- const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
365
- const result = recallAndInline({
366
- sessionId: sid,
367
- query,
368
- limit: config.autoInlineK,
369
- source,
370
- skipInjected: true,
371
- recallMaxTokens: config.recallMaxTokens,
372
- windowDedupe: config.windowDedupe,
373
- liveWindow,
374
- dedupSim: config.dedupSim,
375
- }, runtime.store);
376
- runtime.dashboard.event("recall", { source, query: query.slice(0, 120), injected: result.toInject.length, empty: result.empty });
377
- if (!result.empty && result.toInject.length > 0) {
378
- const top = result.toInject[0];
379
- const scorePct = Math.round((top.score ?? 0) * 100);
380
- const files = top.checkpoint.filesModified ?? [];
381
- const label = files.length ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ") : top.checkpoint.checkpointId;
382
- runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
383
- runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
384
- }
385
- if (result.toInject.length > 0) {
386
- let sumTokens = 0;
387
- for (const h of result.toInject)
388
- sumTokens += h.checkpoint.tokenEstimate;
389
- runtime.rt.recallInjections += result.toInject.length;
390
- runtime.rt.cacheHitTokens += sumTokens;
391
- incRecallInjected(result.toInject.length, runtime.currentStateDir);
392
- incCacheHitTokens(sumTokens, runtime.currentStateDir);
393
- }
394
- return result;
395
- }
396
- /**
397
- * S17: async recall with optional cross-repo augmentation. Used on resume
398
- * (session_start) and /mega-recall --cross-repo — NEVER from the mid-turn
399
- * context handler (that stays sync). Runs the sync same-repo scan first; if it
400
- * returns < config.autoInlineK hits AND crossRepo is enabled, awaits the PGlite
401
- * HNSW cross-repo path and merges (source-labeled, deduped by checkpointId). The
402
- * recallMaxTokens cap + windowDedupe apply to the merged set so cross-repo can
403
- * never net-inflate the window. Cross-repo uses a stricter cosine floor
404
- * (config.crossRepoCosine) than same-repo. Non-fatal: any async failure returns
405
- * the same-repo result unchanged.
406
- */
407
- export async function doRecallAsync(runtime, config, ctx, query, source, opts = {}) {
408
- runtime.bindRepo(ctx.cwd);
409
- const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
410
- const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
411
- // Sync same-repo first (fast, never blocks).
412
- const sameRepo = recallAndInline({
413
- sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
414
- recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
415
- liveWindow, dedupSim: config.dedupSim,
416
- }, runtime.store);
417
- if (!config.crossRepoEnabled || !opts.crossRepo)
418
- return sameRepo;
419
- if (sameRepo.toInject.length >= config.autoInlineK)
420
- return sameRepo; // same-repo satisfied
421
- // Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
422
- try {
423
- const x = await recallAndInlineAsync({
424
- sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
425
- recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
426
- liveWindow, dedupSim: config.crossRepoCosine, crossRepo: true,
427
- globalIndexDir: process.env.MEGACOMPACT_INDEX_DIR,
428
- }, runtime.store);
429
- runtime.dashboard.event("recall-crossrepo", {
430
- source, query: query.slice(0, 120), injected: x.toInject.length,
431
- sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
432
- });
433
- // Merge, dedup by checkpointId, respect the same token cap by reformatting.
434
- const seen = new Set(sameRepo.toInject.map((h) => h.checkpoint.checkpointId));
435
- const merged = [...sameRepo.toInject];
436
- for (const h of x.toInject) {
437
- if (!seen.has(h.checkpoint.checkpointId)) {
438
- merged.push(h);
439
- seen.add(h.checkpoint.checkpointId);
440
- }
441
- }
442
- const block = merged.length ? formatRecallBlock(merged) : "";
443
- if (merged.length > 0) {
444
- let sumTokens = 0;
445
- for (const h of merged)
446
- sumTokens += h.checkpoint.tokenEstimate;
447
- runtime.rt.recallInjections += merged.length;
448
- runtime.rt.cacheHitTokens += sumTokens;
449
- incRecallInjected(merged.length, runtime.currentStateDir);
450
- incCacheHitTokens(sumTokens, runtime.currentStateDir);
451
- }
452
- return {
453
- toInject: merged,
454
- report: merged.map((h) => ` • ${h.checkpoint.checkpointId}${h.repoId ? ` (from ${h.repoId.split("/").filter(Boolean).pop()})` : ""}`),
455
- block,
456
- empty: merged.length === 0,
457
- };
458
- }
459
- catch {
460
- return sameRepo; // cross-repo failure → same-repo only (non-fatal)
461
- }
462
- }
463
- /**
464
- * Extract the live-window message texts from the session manager (Fix C),
465
- * for inline-dedupe of recalled checkpoints. Best-effort: returns [] on any
466
- * error so recall falls back to unbounded (still correct, just no dedupe).
467
- * Mirrors recentUserQuery's use of sessionEntryToContextMessages.
468
- */
469
- function extractLiveWindow(ctx) {
470
- try {
471
- const entries = ctx.sessionManager.getEntries();
472
- const texts = [];
473
- for (const e of entries) {
474
- for (const m of sessionEntryToContextMessages(e)) {
475
- const c = m.content;
476
- if (typeof c === "string")
477
- texts.push(c);
478
- else if (Array.isArray(c))
479
- texts.push(c.map((b) => b.text).join(" "));
480
- }
481
- }
482
- return texts;
483
- }
484
- catch {
485
- return [];
486
- }
487
- }
14
+ export * from "./mega-pipeline/memory-review.js";
15
+ export * from "./mega-pipeline/compact.js";
16
+ export * from "./mega-pipeline/recall.js";