pi-mega-compact 0.7.1 → 0.7.3

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.
@@ -8,13 +8,13 @@
8
8
  */
9
9
  import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
10
10
  import { compactSession } from "../src/engine.js";
11
- import { recallAndInline, recallAndInlineAsync, formatRecallBlock, } from "../src/recall.js";
11
+ import { recallAndInline, recallAndInlineAsync, formatRecallBlock } from "../src/recall.js";
12
12
  import { normalizeSessionId } from "../src/store.js";
13
13
  import { estimateBlockTokens } from "../src/tokens.js";
14
14
  import { touchSession, logDaily } from "../src/store/sqlite.js";
15
15
  import { consolidateMemories } from "../src/memory.js";
16
- import { C, MARKER_TYPE } from "./mega-runtime.js";
17
- import { resolveRepoRoot, preserveRecentForPressure, } from "./mega-config.js";
16
+ import { C, MARKER_TYPE, } from "./mega-runtime.js";
17
+ import { resolveRepoRoot, preserveRecentForPressure } from "./mega-config.js";
18
18
  import { runRaptor } from "../src/dedup/raptor/index.js";
19
19
  import { loadDedupConfig } from "../src/config/dedup.js";
20
20
  import { upsertEmbedding as indexUpsertEmbedding } from "../src/store/vectorIndex.js";
@@ -110,17 +110,13 @@ function doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime) {
110
110
  // Grow the rolling "saved" goal so the progress bar always has a fresh
111
111
  // denominator (we don't want it pinned at 100% once we pass an old target).
112
112
  if (runtime.rt.tokensSaved > runtime.savedGoal)
113
- runtime.savedGoal =
114
- Math.ceil((runtime.rt.tokensSaved * 1.25) / 10_000) * 10_000;
113
+ runtime.savedGoal = Math.ceil((runtime.rt.tokensSaved * 1.25) / 10_000) * 10_000;
115
114
  // Live toolbar activity: what file/region just got compacted or deduped.
116
115
  // Rendered via the rotating ticker line (see snapshot); the ring buffer is
117
116
  // cycled one-per-repaint so the single line scrolls through recent files.
118
117
  const files = result.filesModified ?? [];
119
118
  const fileLabel = files.length
120
- ? files
121
- .map((f) => f.split("/").pop() ?? f)
122
- .slice(0, 2)
123
- .join(", ")
119
+ ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ")
124
120
  : result.regionHash.slice(0, 8);
125
121
  runtime.lastActivityAt = Date.now();
126
122
  // Explain-why line: surfaced while fresh. Pulls the dedup reason (which for
@@ -172,10 +168,7 @@ function doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime) {
172
168
  // waiting for the next turn-cadence tick. Uses the shared runMemoryReview
173
169
  // helper (fire-and-forget; doCompact is sync). Best-effort + non-fatal. Only
174
170
  // fires above the `high` band so low-pressure compactions don't pay the cost.
175
- if (!result.deduped &&
176
- config.memoryAutoReview &&
177
- runtime.pressureBand !== "low" &&
178
- runtime.pressureBand !== "medium") {
171
+ if (!result.deduped && config.memoryAutoReview && runtime.pressureBand !== "low" && runtime.pressureBand !== "medium") {
179
172
  void runMemoryReview(runtime, view, "pressure");
180
173
  }
181
174
  // Sentinel marker: a non-LLM bookkeeping entry so subsequent triggers can
@@ -203,9 +196,7 @@ function doCompact(view, keepFrom, opts, sid, config, pi, ctx, runtime) {
203
196
  // S25: stamp the tree with the newest checkpoint epoch so the
204
197
  // freshness guard in raptorSearchHits can reject stale trees after a
205
198
  // later compaction adds newer checkpoints.
206
- const builtAt = all.length > 0
207
- ? Math.max(...all.map((c) => c.timestamp))
208
- : Date.now();
199
+ const builtAt = all.length > 0 ? Math.max(...all.map((c) => c.timestamp)) : Date.now();
209
200
  runRaptor(leaves, {
210
201
  stateDir: runtime.currentStateDir,
211
202
  sessionId: sid,
@@ -323,8 +314,7 @@ export function piCompactWouldNoop(ctx) {
323
314
  if (m.role !== "toolResult")
324
315
  isCut = true;
325
316
  const c = m.content;
326
- const text = typeof c === "string"
327
- ? c
317
+ const text = typeof c === "string" ? c
328
318
  : Array.isArray(c)
329
319
  ? c.map((b) => b?.text ?? "").join(" ")
330
320
  : "";
@@ -377,22 +367,12 @@ export function doRecall(runtime, config, ctx, query, source) {
377
367
  liveWindow,
378
368
  dedupSim: config.dedupSim,
379
369
  }, runtime.store);
380
- runtime.dashboard.event("recall", {
381
- source,
382
- query: query.slice(0, 120),
383
- injected: result.toInject.length,
384
- empty: result.empty,
385
- });
370
+ runtime.dashboard.event("recall", { source, query: query.slice(0, 120), injected: result.toInject.length, empty: result.empty });
386
371
  if (!result.empty && result.toInject.length > 0) {
387
372
  const top = result.toInject[0];
388
373
  const scorePct = Math.round((top.score ?? 0) * 100);
389
374
  const files = top.checkpoint.filesModified ?? [];
390
- const label = files.length
391
- ? files
392
- .map((f) => f.split("/").pop() ?? f)
393
- .slice(0, 2)
394
- .join(", ")
395
- : top.checkpoint.checkpointId;
375
+ const label = files.length ? files.map((f) => f.split("/").pop() ?? f).slice(0, 2).join(", ") : top.checkpoint.checkpointId;
396
376
  runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
397
377
  runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
398
378
  }
@@ -415,15 +395,9 @@ export async function doRecallAsync(runtime, config, ctx, query, source, opts =
415
395
  const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
416
396
  // Sync same-repo first (fast, never blocks).
417
397
  const sameRepo = recallAndInline({
418
- sessionId: sid,
419
- query,
420
- limit: config.autoInlineK,
421
- source,
422
- skipInjected: true,
423
- recallMaxTokens: config.recallMaxTokens,
424
- windowDedupe: config.windowDedupe,
425
- liveWindow,
426
- dedupSim: config.dedupSim,
398
+ sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
399
+ recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
400
+ liveWindow, dedupSim: config.dedupSim,
427
401
  }, runtime.store);
428
402
  if (!config.crossRepoEnabled || !opts.crossRepo)
429
403
  return sameRepo;
@@ -432,22 +406,13 @@ export async function doRecallAsync(runtime, config, ctx, query, source, opts =
432
406
  // Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
433
407
  try {
434
408
  const x = await recallAndInlineAsync({
435
- sessionId: sid,
436
- query,
437
- limit: config.autoInlineK,
438
- source,
439
- skipInjected: true,
440
- recallMaxTokens: config.recallMaxTokens,
441
- windowDedupe: config.windowDedupe,
442
- liveWindow,
443
- dedupSim: config.crossRepoCosine,
444
- crossRepo: true,
409
+ sessionId: sid, query, limit: config.autoInlineK, source, skipInjected: true,
410
+ recallMaxTokens: config.recallMaxTokens, windowDedupe: config.windowDedupe,
411
+ liveWindow, dedupSim: config.crossRepoCosine, crossRepo: true,
445
412
  globalIndexDir: process.env.MEGACOMPACT_INDEX_DIR,
446
413
  }, runtime.store);
447
414
  runtime.dashboard.event("recall-crossrepo", {
448
- source,
449
- query: query.slice(0, 120),
450
- injected: x.toInject.length,
415
+ source, query: query.slice(0, 120), injected: x.toInject.length,
451
416
  sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
452
417
  });
453
418
  // Merge, dedup by checkpointId, respect the same token cap by reformatting.
@@ -93,6 +93,33 @@ function visibleWidth(s) {
93
93
  return w;
94
94
  }
95
95
  /** Pad a content string (with ANSI colors) to `width` cells using panel bg. */
96
+ /** Wrap a string (with ANSI codes) to fit within `maxWidth` visible chars.
97
+ * Splits at │ separators or whitespace when possible. */
98
+ function wrapLine(text, maxWidth) {
99
+ if (maxWidth <= 0)
100
+ return [text];
101
+ const result = [];
102
+ let current = "";
103
+ let currentW = 0;
104
+ // Split at │ boundaries first
105
+ const segments = text.split("│");
106
+ for (let i = 0; i < segments.length; i++) {
107
+ const seg = (i > 0 ? "│" : "") + segments[i];
108
+ const segW = visibleWidth(PANEL_BG + seg.replace(/\x1b\[0m/g, PANEL_RST));
109
+ if (currentW + segW <= maxWidth || currentW === 0) {
110
+ current += seg;
111
+ currentW += segW;
112
+ }
113
+ else {
114
+ result.push(current);
115
+ current = seg;
116
+ currentW = segW;
117
+ }
118
+ }
119
+ if (current)
120
+ result.push(current);
121
+ return result;
122
+ }
96
123
  function panelLine(content, width) {
97
124
  const withBg = PANEL_BG + content.replace(/\x1b\[0m/g, PANEL_RST);
98
125
  const pad = Math.max(0, width - visibleWidth(withBg));
@@ -603,15 +630,26 @@ export class MegaRuntime {
603
630
  const pulse = wd.pulsing
604
631
  ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} `
605
632
  : "";
633
+ const sep = ` ${C.dim}│${C.reset} `;
634
+ // Build one long content line — let terminal wrap it naturally
635
+ const content = [
636
+ `${C.amber}⚡ ${wd.tierLabel}${C.reset} v${C.bold}${wd.version}${C.reset} ${ramp(wd.ctxPct, 20)} ${C.bold}${wd.pctStr}${C.reset} ${wd.tokStr}/${wd.maxStr}`,
637
+ wd.triggerLabel,
638
+ `${C.cyan}${wd.modelStr}${C.reset}`,
639
+ `${wd.chk} chk${wd.agentStr}${wd.turnStr}`,
640
+ `${C.magenta}dup ${wd.dedupStr}${C.reset}`,
641
+ `${C.gray}sess${C.reset} ${fmtTokens(wd.sessIn)}→${fmtTokens(wd.sessKept)} kept ${C.green}(${wd.sTxt}% freed)${C.reset}`,
642
+ `${C.gray}all-time${C.reset} ${fmtTokens(wd.repoIn)}→${fmtTokens(wd.repoKept)} kept ${C.blue}(${wd.rTxt}% freed)${C.reset}`,
643
+ `${wd.repoChk} chk/${wd.repoSess} sess`,
644
+ `${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset}`,
645
+ `${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset}`,
646
+ `${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`,
647
+ ].join(sep);
648
+ // Wrap to terminal width and pad each line
649
+ const wrapped = wrapLine(content, width - 2); // 2-char indent
606
650
  const lines = [
607
- // top border
608
651
  panelBar(width, "─"),
609
- // L1 header: tier + ctx bar + pct/tokens + status + model + chk + agents/turn
610
- panelLine(` ${C.amber}⚡ ${wd.tierLabel}${C.reset} v${C.bold}${wd.version}${C.reset} ${ramp(wd.ctxPct, 20)} ${C.bold}${wd.pctStr}${C.reset} ${wd.tokStr}/${wd.maxStr} │ ${wd.triggerLabel} │ ${C.cyan}${wd.modelStr}${C.reset} │ ${wd.chk} chk${wd.agentStr}${wd.turnStr}`, width),
611
- // L2 — savings reconciled (session + all-time)
612
- panelLine(` ${C.magenta}dup ${wd.dedupStr}${C.reset} │ ${C.gray}sess${C.reset} ${fmtTokens(wd.sessIn)}→${fmtTokens(wd.sessKept)} kept ${C.green}(${wd.sTxt}% freed)${C.reset} · ${C.gray}all-time${C.reset} ${fmtTokens(wd.repoIn)}→${fmtTokens(wd.repoKept)} kept ${C.blue}(${wd.rTxt}% freed)${C.reset} │ ${wd.repoChk} chk/${wd.repoSess} sess`, width),
613
- // L3 — memory store + compression + drift + since-compact (NEW)
614
- panelLine(` ${C.gray}mem${C.reset} ${wd.embedderName} · ${wd.chk} chunks · ${C.blue}comp ${wd.compStr}${C.reset} │ ${C.gray}drift${C.reset} ${wd.driftStatus === "ok" ? C.green : C.amber}${wd.driftStatus}${C.reset} │ ${C.gray}compact${C.reset} ${sinceCompactStr(wd.sinceCompact)}`, width),
652
+ ...wrapped.map((l) => panelLine(l, width)),
615
653
  ];
616
654
  // L4 — agents block (S27, count + status; per-agent tokens gated on P0)
617
655
  if (wd.agentsActive) {
@@ -30,13 +30,13 @@ import type { MegaConfig } from "./mega-config.js";
30
30
  import { recallRaptorRootSummary } from "../src/dedup/raptor/index.js";
31
31
 
32
32
  export interface NativeCompactionResult {
33
- /** Our trimmed summary + the pi entry to keep from (durable trim). */
34
- compaction: {
35
- summary: string;
36
- firstKeptEntryId: string;
37
- tokensBefore: number;
38
- estimatedTokensAfter: number;
39
- };
33
+ /** Our trimmed summary + the pi entry to keep from (durable trim). */
34
+ compaction: {
35
+ summary: string;
36
+ firstKeptEntryId: string;
37
+ tokensBefore: number;
38
+ estimatedTokensAfter: number;
39
+ };
40
40
  }
41
41
 
42
42
  /**
@@ -46,61 +46,61 @@ export interface NativeCompactionResult {
46
46
  * own native compaction, or skip). Never throws for "empty" — best-effort.
47
47
  */
48
48
  export function driveNativeCompaction(
49
- event: SessionBeforeCompactEvent,
50
- runtime: MegaRuntime,
51
- config: MegaConfig,
49
+ event: SessionBeforeCompactEvent,
50
+ runtime: MegaRuntime,
51
+ config: MegaConfig,
52
52
  ): NativeCompactionResult | undefined {
53
- const prep = event.preparation;
54
- if (!prep) return undefined;
53
+ const prep = event.preparation;
54
+ if (!prep) return undefined;
55
55
 
56
- const sid = runtime.rt.sessionId;
57
- const messagesToSummarize: AgentMessage[] = prep.messagesToSummarize ?? [];
58
- if (messagesToSummarize.length === 0) return undefined;
56
+ const sid = runtime.rt.sessionId;
57
+ const messagesToSummarize: AgentMessage[] = prep.messagesToSummarize ?? [];
58
+ if (messagesToSummarize.length === 0) return undefined;
59
59
 
60
- const engineView = toEngineMessages(messagesToSummarize);
61
- // We don't drop anything here — pi keeps from prep.firstKeptEntryId. We only
62
- // summarize the region pi is about to discard.
63
- const keepFrom = engineView.length;
60
+ const engineView = toEngineMessages(messagesToSummarize);
61
+ // We don't drop anything here — pi keeps from prep.firstKeptEntryId. We only
62
+ // summarize the region pi is about to discard.
63
+ const keepFrom = engineView.length;
64
64
 
65
- const result = compactSession(
66
- {
67
- sessionId: sid,
68
- messages: engineView,
69
- keepFrom,
70
- timestamp: Date.now(),
71
- useExtractiveSummary: true,
72
- },
73
- runtime.store,
74
- );
75
- if (result.skipped) return undefined;
65
+ const result = compactSession(
66
+ {
67
+ sessionId: sid,
68
+ messages: engineView,
69
+ keepFrom,
70
+ timestamp: Date.now(),
71
+ useExtractiveSummary: true,
72
+ },
73
+ runtime.store,
74
+ );
75
+ if (result.skipped) return undefined;
76
76
 
77
- // Prefer the RAPTOR root summary when the tree is built + enabled (Fix D):
78
- // it is a session-level compressed summary, broader than one slice's. Fall
79
- // back to the extractive topicSummary of this slice.
80
- let summary = result.summary;
81
- if (config.raptorEnabled) {
82
- const root = recallRaptorRootSummary(sid, runtime.currentStateDir);
83
- if (root) summary = root;
84
- }
77
+ // Prefer the RAPTOR root summary when the tree is built + enabled (Fix D):
78
+ // it is a session-level compressed summary, broader than one slice's. Fall
79
+ // back to the extractive topicSummary of this slice.
80
+ let summary = result.summary;
81
+ if (config.raptorEnabled) {
82
+ const root = recallRaptorRootSummary(sid, runtime.currentStateDir);
83
+ if (root) summary = root;
84
+ }
85
85
 
86
- const tokensBefore = prep.tokensBefore ?? estimateSessionTokens(engineView);
87
- const summaryTokens = estimateBlockTokens(summary);
88
- // pi keeps the tail from firstKeptEntryId; our summary replaces the discarded
89
- // region. Honest saved = discarded-region tokens − our summary tokens.
90
- const savedTokens = Math.max(0, tokensBefore - summaryTokens);
86
+ const tokensBefore = prep.tokensBefore ?? estimateSessionTokens(engineView);
87
+ const summaryTokens = estimateBlockTokens(summary);
88
+ // pi keeps the tail from firstKeptEntryId; our summary replaces the discarded
89
+ // region. Honest saved = discarded-region tokens − our summary tokens.
90
+ const savedTokens = Math.max(0, tokensBefore - summaryTokens);
91
91
 
92
- runtime.rt.lastCompactedFrom = keepFrom;
93
- runtime.rt.lastCompactedTokens = tokensBefore;
94
- runtime.rt.tokensSaved += savedTokens;
95
- runtime.rt.lastCompactAt = Date.now();
96
- runtime.rt.persistedThisSession = true;
92
+ runtime.rt.lastCompactedFrom = keepFrom;
93
+ runtime.rt.lastCompactedTokens = tokensBefore;
94
+ runtime.rt.tokensSaved += savedTokens;
95
+ runtime.rt.lastCompactAt = Date.now();
96
+ runtime.rt.persistedThisSession = true;
97
97
 
98
- return {
99
- compaction: {
100
- summary,
101
- firstKeptEntryId: prep.firstKeptEntryId,
102
- tokensBefore,
103
- estimatedTokensAfter: summaryTokens,
104
- },
105
- };
98
+ return {
99
+ compaction: {
100
+ summary,
101
+ firstKeptEntryId: prep.firstKeptEntryId,
102
+ tokensBefore,
103
+ estimatedTokensAfter: summaryTokens,
104
+ },
105
+ };
106
106
  }