pi-mega-compact 0.20.86 → 0.20.87

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * src/recall/recall3wf.fixture.ts — shared fixtures for the 3WF-3 recall tests.
3
+ *
4
+ * Split out of recall3wf.test.ts (which crossed the src 300 soft cap) so each
5
+ * test file stays under the limit. These are REAL fixtures, not mocks/stubs:
6
+ * a REAL VectorStore over a temp stateDir, REAL checkpoints persisted via
7
+ * compactSession, and readers that go through the SAME working path the
8
+ * extension uses (recallRawHits -> vectorSearch -> listCheckpoints, and
9
+ * vectorWasInjected), mirroring the proven triggerGuard test pattern.
10
+ */
11
+ import { mkdtempSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { VectorStore } from "../vectorStore.js";
15
+ import { compactSession } from "../engine.js";
16
+ import { recallAndInline } from "../recall.js";
17
+ import { recallRawHits } from "./readonly.js";
18
+ import { openStore } from "../store/sqlite/utils.js";
19
+ import { initSchema } from "../store/sqlite/schema.js";
20
+ /** Real EngineMessage fixture. */
21
+ export function msg(role, text) {
22
+ return { role, text };
23
+ }
24
+ /** Fresh isolated state dir per VectorStore. */
25
+ export function freshStore() {
26
+ const dir = mkdtempSync(join(tmpdir(), "mc-3wf-"));
27
+ return { store: new VectorStore({ dedupSim: 0.9, stateDir: dir }), dir };
28
+ }
29
+ /** Persist N distinct checkpoints with distinct content + ascending timestamps. */
30
+ export function seed(store, topics, sid = "sess_3wf") {
31
+ topics.forEach((t, i) => {
32
+ compactSession({
33
+ sessionId: sid,
34
+ messages: [msg("user", t), msg("assistant", "ok")],
35
+ keepFrom: 2,
36
+ timestamp: i + 1,
37
+ }, store);
38
+ });
39
+ }
40
+ /** Checkpoint ids via the real search path (vectorSearch -> listCheckpoints). */
41
+ export function checkpointIds(store, sid, query) {
42
+ return recallRawHits({ sessionId: sid, query, limit: 10 }, store).map((h) => h.checkpoint.checkpointId);
43
+ }
44
+ /** Run the real recallAndInline path with skipInjected:false so nothing is
45
+ * marked and the block reflects the search result exactly (deterministic). */
46
+ export function recallAndInlineCapture(sid, query, store) {
47
+ const r = recallAndInline({ sessionId: sid, query, limit: 3, source: "command", skipInjected: false, windowDedupe: false }, store);
48
+ return { block: r.block, empty: r.empty, toInject: r.toInject };
49
+ }
50
+ /** Count recall-provenance rows (turn_recall) for a session via raw SQL reader. */
51
+ export function countTurnRecallRows(store, sid) {
52
+ try {
53
+ const reader = openStore(store.stateDir);
54
+ // Ensure the turns/turn_recall tables exist so a 0-count is meaningful
55
+ // (a write on the new path would be visible, not masked by a missing table).
56
+ initSchema(reader);
57
+ const row = reader
58
+ .prepare(`SELECT COUNT(*) AS n FROM turn_recall tr
59
+ JOIN turns t ON t.id = tr.turn_id
60
+ WHERE t.session_id = ?`)
61
+ .get(sid);
62
+ return row?.n ?? 0;
63
+ }
64
+ catch {
65
+ return 0;
66
+ }
67
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * recall/validator.ts — independent candidate validator (3WF-3).
3
+ *
4
+ * Judges candidates handed to it; it MUST NOT call any search itself. Given the
5
+ * ranked vote winners + the live-window text (already extracted by the caller,
6
+ * since src/ cannot import pi types), it walks the winners in order and returns
7
+ * the first that passes BOTH gates:
8
+ *
9
+ * 1. Cosine floor: the winner's score >= the same-repo floor (default 0.12,
10
+ * env MEGACOMPACT_RECALL_MIN_COSINE). The cross-repo 0.90 floor
11
+ * (config.crossRepoCosine) is SEPARATE and intentionally untouched.
12
+ * 2. Not already resident in the live window: reuse recall/sync.ts's exact
13
+ * comparison — embed each live message, embed the checkpoint summary, and
14
+ * treat the checkpoint as resident when cosineSimilarity >= dedupSim. We
15
+ * reuse that metric rather than inventing a new one.
16
+ *
17
+ * On a failing candidate it advances to the next-ranked winner. If ALL fail it
18
+ * returns the provenance floor (FloorBlock built from the newest checkpoint —
19
+ * pure over checkpoints, same semantics as triggerGuard's buildFloorBlock).
20
+ *
21
+ * Non-fatal throughout: any error degrades to the next candidate / the floor.
22
+ * Pi-agnostic: no pi runtime imports.
23
+ */
24
+ import { defaultEmbedder, cosineSimilarity } from "../embedder.js";
25
+ // SQLite store, NOT src/store.ts's legacy gzipped-JSON DR reader (that returns
26
+ // [] for live sessions). Mirrors vector-search.ts / tieredRouter.ts.
27
+ import { listCheckpoints } from "../store/sqlite.js";
28
+ import { RECALL_MIN_COSINE } from "../config.js";
29
+ /** Build the provenance floor block from the session's newest checkpoint. */
30
+ function buildFloorBlock(sessionId, store) {
31
+ try {
32
+ const cps = listCheckpoints(sessionId, store.stateDir).filter((c) => c.dedupStatus !== "removed");
33
+ let newest = cps[0];
34
+ for (const cp of cps) {
35
+ if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0))
36
+ newest = cp;
37
+ }
38
+ const summary = newest?.summary?.trim();
39
+ if (summary) {
40
+ return {
41
+ text: "The following compacted context is the most recent checkpoint from " +
42
+ "this session (recall found no query-relevant match):\n\n" + summary,
43
+ basis: "lastCheckpoint",
44
+ };
45
+ }
46
+ return {
47
+ text: "This session has compacted context but recall could not surface a " +
48
+ "checkpoint relevant to the current request; the most recent checkpoint " +
49
+ "summary is unavailable.",
50
+ basis: "lastCheckpoint",
51
+ };
52
+ }
53
+ catch {
54
+ return {
55
+ text: "This session has compacted context but recall could not surface a " +
56
+ "checkpoint relevant to the current request.",
57
+ basis: "none",
58
+ };
59
+ }
60
+ }
61
+ /**
62
+ * Validate the ranked vote winners, returning the first that passes both gates,
63
+ * or the provenance floor if none do. Does NOT mutate the injected set, does NOT
64
+ * write turns, does NOT emit telemetry. Non-fatal.
65
+ */
66
+ export function validateRecall(winners, opts, store) {
67
+ const floor = RECALL_MIN_COSINE();
68
+ const dedupSim = opts.dedupSim ?? 0.9;
69
+ const embedder = defaultEmbedder();
70
+ const liveVecs = (opts.liveWindow ?? []).map((m) => embedder.embed(m));
71
+ // One checkpoint read for the whole pass (both gates share it).
72
+ const cps = listCheckpoints(opts.sessionId, store.stateDir);
73
+ const cpById = new Map(cps.map((c) => [c.checkpointId, c]));
74
+ const queryVec = opts.query ? embedder.embed(opts.query) : null;
75
+ for (const cand of winners) {
76
+ try {
77
+ const cp = cpById.get(cand.checkpointId);
78
+ // Gate 1: same-repo COSINE floor. `cand.score` is only a cosine for
79
+ // source "vector"; fts5 (BM25) and recency (freshness rank) live on
80
+ // other scales, so for those we re-derive the true cosine locally from
81
+ // the query + checkpoint embedding. No search call is made.
82
+ let cosine;
83
+ if (cand.source === "vector") {
84
+ cosine = cand.score;
85
+ }
86
+ else if (queryVec && cp) {
87
+ cosine = cosineSimilarity(queryVec, embedder.embed(cp.summary));
88
+ }
89
+ else {
90
+ // No comparable cosine available => cannot clear a cosine gate.
91
+ continue;
92
+ }
93
+ if (cosine < floor)
94
+ continue;
95
+ // Gate 2: not already resident in the live window.
96
+ if (liveVecs.length > 0) {
97
+ if (!cp)
98
+ continue; // cannot verify => skip rather than risk re-inject
99
+ const hitVec = embedder.embed(cp.summary);
100
+ const resident = liveVecs.some((v) => cosineSimilarity(v, hitVec) >= dedupSim);
101
+ if (resident)
102
+ continue;
103
+ }
104
+ return { kind: "candidate", candidate: cand };
105
+ }
106
+ catch {
107
+ // Non-fatal: skip this candidate, try the next.
108
+ continue;
109
+ }
110
+ }
111
+ // All candidates rejected -> provenance floor.
112
+ return { kind: "floor", floor: buildFloorBlock(opts.sessionId, store) };
113
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * recall/vote.ts — the 3-independent-source recall vote (3WF-3).
3
+ *
4
+ * Three INDEPENDENT, read-only sources name candidate checkpoints:
5
+ * A vector — raw semantic hits (recall/readonly.ts), cosine 0..1 scale.
6
+ * B fts5 — BM25 trigram hits (hydrated to checkpointIds), FTS5 BM25 scale
7
+ * (negative score = better match; ranking order is what matters).
8
+ * C recency — the N freshest checkpoints by timestamp, query-INDEPENDENT.
9
+ * (NOT turn_recall / TurnReader — that would just echo already-
10
+ * injected content, not an independent signal.)
11
+ *
12
+ * Each source is a different score scale, so they are NOT directly comparable.
13
+ * We normalize each source's scores to a 0..1 scale (per-source min-max) BEFORE
14
+ * combining. Averaging raw cosine (0..1) with raw BM25 (arbitrary negative
15
+ * magnitude) or a recency rank would be meaningless — the largest-magnitude
16
+ * scale would always dominate. Normalization makes each source a peer voter.
17
+ *
18
+ * Overlap rule: a checkpoint named by >=2 of 3 distinct sources short-circuits
19
+ * as a winner. Fallback (no 2/3 majority): rank all candidates by the
20
+ * cross-source MEAN of their normalized scores.
21
+ *
22
+ * Non-fatal throughout. Pi-agnostic: no pi runtime imports.
23
+ */
24
+ import { openStore } from "../store/sqlite/utils.js";
25
+ import { fts5SearchScoped, hydrateFts5Hits } from "../store/sqlite/fts5-search.js";
26
+ // The SQLite store is the source of truth (src/store.ts's same-named helper
27
+ // reads the LEGACY gzipped-JSON DR snapshot, which is empty for live sessions —
28
+ // importing it here would silently starve sources B and C). Mirrors the import
29
+ // in vector-search.ts + tieredRouter.ts.
30
+ import { listCheckpoints } from "../store/sqlite.js";
31
+ import { computeContentDigest } from "../dedup/digest.js";
32
+ import { recallRawHits } from "./readonly.js";
33
+ import { Logger } from "../log.js";
34
+ /** Per-source normalization: map raw scores to 0..1 via min-max within source. */
35
+ function normalizeScores(scores) {
36
+ const map = new Map();
37
+ if (scores.length === 0)
38
+ return map;
39
+ const min = Math.min(...scores);
40
+ const max = Math.max(...scores);
41
+ const span = max - min;
42
+ scores.forEach((s, i) => {
43
+ map.set(i, span === 0 ? 1 : (s - min) / span);
44
+ });
45
+ return map;
46
+ }
47
+ /**
48
+ * Run the three-source recall vote. Returns agreement winners + a per-id vote
49
+ * count + the names of sources that produced no winning candidate. Non-fatal:
50
+ * any search failure degrades to the remaining sources (empty winners allowed).
51
+ */
52
+ export function voteRecall(opts, store) {
53
+ const logger = new Logger();
54
+ const limit = opts.limit ?? 3;
55
+ const recencyCount = opts.recencyCount ?? limit;
56
+ // ── Source A: vector (raw hits, cosine 0..1). ────────────────────────────
57
+ const vectorCands = recallRawHits({ sessionId: opts.sessionId, query: opts.query, limit }, store).map((h) => ({
58
+ checkpointId: h.checkpoint.checkpointId,
59
+ score: h.score,
60
+ source: "vector",
61
+ }));
62
+ // ── Source B: fts5 (BM25, hydrated to checkpointIds). ─────────────────────
63
+ // Dedup on L0 content digest so the SAME normalized text under two ids does
64
+ // not double-vote — collapse to one candidate per digest.
65
+ const fts5Cands = (() => {
66
+ try {
67
+ const reader = openStore(store.stateDir);
68
+ const hits = fts5SearchScoped(opts.query, reader, opts.sessionId, limit);
69
+ // FTS5 returns scores ordered best-first (bm25 asc); lower = better.
70
+ // We keep the raw score (negative-is-better) and flip in normalization.
71
+ const hydrated = hydrateFts5Hits(hits, opts.sessionId, store.stateDir);
72
+ // Dedup on checkpointId so the same checkpoint cannot double-count, AND
73
+ // on the L0 CONTENT digest so identical normalized text stored under two
74
+ // different ids collapses to one vote. The digest is taken over the
75
+ // joined `summary` (real content): hashing the id string would be a
76
+ // no-op tier, since ids are unique by definition.
77
+ const seenDigest = new Set();
78
+ const seenId = new Set();
79
+ const out = [];
80
+ for (const h of hydrated) {
81
+ if (seenId.has(h.checkpointId))
82
+ continue;
83
+ seenId.add(h.checkpointId);
84
+ const digest = computeContentDigest(h.summary).contentHash;
85
+ if (seenDigest.has(digest))
86
+ continue;
87
+ seenDigest.add(digest);
88
+ out.push({ checkpointId: h.checkpointId, score: h.score, source: "fts5" });
89
+ }
90
+ return out;
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ })();
96
+ // ── Source C: recency (N freshest checkpoints, query-independent). ────────
97
+ // Timestamp-ordered; the lower the index the fresher. Score = recency rank
98
+ // (fresh = high) so normalization treats newest as best.
99
+ const recencyCands = (() => {
100
+ try {
101
+ const cps = listCheckpoints(opts.sessionId, store.stateDir)
102
+ .filter((c) => c.dedupStatus !== "removed")
103
+ .sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
104
+ .slice(0, recencyCount);
105
+ return cps.map((cp, i) => ({
106
+ checkpointId: cp.checkpointId,
107
+ // Fresher => higher raw score (recency rank). Normalized below.
108
+ score: cps.length - i,
109
+ source: "recency",
110
+ }));
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ })();
116
+ const sources = [
117
+ { name: "vector", cands: vectorCands },
118
+ { name: "fts5", cands: fts5Cands },
119
+ { name: "recency", cands: recencyCands },
120
+ ];
121
+ // Per-source normalization to 0..1 so the three scales are comparable.
122
+ const perSource = sources.map((s) => ({
123
+ name: s.name,
124
+ norm: normalizeScores(s.cands.map((c) => c.score)),
125
+ }));
126
+ // Aggregate: best normalized score per source per checkpointId + vote count.
127
+ const bestScoreBySource = new Map();
128
+ const seenIds = new Set();
129
+ for (const src of sources) {
130
+ const norm = perSource.find((p) => p.name === src.name).norm;
131
+ const bestByCp = new Map();
132
+ src.cands.forEach((c, i) => {
133
+ const n = norm.get(i) ?? 0;
134
+ const prev = bestByCp.get(c.checkpointId);
135
+ if (prev === undefined || n > prev)
136
+ bestByCp.set(c.checkpointId, n);
137
+ seenIds.add(c.checkpointId);
138
+ });
139
+ bestScoreBySource.set(src.name, bestByCp);
140
+ }
141
+ // Vote count = number of DISTINCT sources naming each checkpointId.
142
+ const votes = {};
143
+ const sumByCp = new Map();
144
+ for (const id of seenIds) {
145
+ let count = 0;
146
+ let sum = 0;
147
+ for (const src of sources) {
148
+ const m = bestScoreBySource.get(src.name);
149
+ if (m.has(id)) {
150
+ count++;
151
+ sum += m.get(id);
152
+ }
153
+ }
154
+ votes[id] = count;
155
+ sumByCp.set(id, sum);
156
+ }
157
+ /** Mean normalized score across the sources that named `id`. */
158
+ const meanScore = (id) => (sumByCp.get(id) ?? 0) / (votes[id] ?? 1);
159
+ // Short-circuit: >=2 of 3 distinct sources => winner (agreement). Ranked by
160
+ // vote count first (stronger agreement wins), then by mean normalized score —
161
+ // the validator consumes this list in order and takes the first that passes,
162
+ // so the ordering IS the ranking and must not be Set-insertion order.
163
+ const winners = [];
164
+ const divergent = new Set(sources.map((s) => s.name));
165
+ const agreed = [...seenIds]
166
+ .filter((id) => (votes[id] ?? 0) >= 2)
167
+ .sort((a, b) => (votes[b] ?? 0) - (votes[a] ?? 0) || meanScore(b) - meanScore(a));
168
+ for (const id of agreed) {
169
+ const cand = (() => {
170
+ for (const src of sources) {
171
+ const c = src.cands.find((x) => x.checkpointId === id);
172
+ if (c)
173
+ return c;
174
+ }
175
+ return null;
176
+ })();
177
+ if (cand)
178
+ winners.push(cand);
179
+ }
180
+ if (winners.length === 0) {
181
+ // Fallback (no 2-of-3 agreement): rank by cross-source MEAN normalized score.
182
+ const ranked = [...seenIds].sort((a, b) => meanScore(b) - meanScore(a));
183
+ for (const id of ranked) {
184
+ const cand = (() => {
185
+ for (const src of sources) {
186
+ const c = src.cands.find((x) => x.checkpointId === id);
187
+ if (c)
188
+ return c;
189
+ }
190
+ return null;
191
+ })();
192
+ if (cand)
193
+ winners.push(cand);
194
+ }
195
+ }
196
+ // Divergence = a source that named NONE of the winning checkpoints. This must
197
+ // be computed per SOURCE against the winning ID SET, not from `winner.source`
198
+ // (a winner is one candidate object carrying a single source label, so an id
199
+ // agreed on by all three sources would still credit only one of them).
200
+ const winningIds = new Set(winners.map((w) => w.checkpointId));
201
+ for (const src of sources) {
202
+ if (src.cands.some((c) => winningIds.has(c.checkpointId))) {
203
+ divergent.delete(src.name);
204
+ }
205
+ }
206
+ if (divergent.size > 0) {
207
+ logger.info("recall_vote_divergence", {
208
+ divergentSources: [...divergent],
209
+ winnerCount: winners.length,
210
+ });
211
+ }
212
+ return {
213
+ winners,
214
+ votes,
215
+ divergentSources: [...divergent],
216
+ };
217
+ }
@@ -11,6 +11,7 @@
11
11
  * time automatically — no manual n-gram splitting needed. The BM25 rank from
12
12
  * fts5 ranks results by trigram-overlap density.
13
13
  */
14
+ import { listCheckpoints } from "./checkpoints.js";
14
15
  /**
15
16
  * Search the context_chunks_trgm FTS5 table for checkpoints whose
16
17
  * `normalized_text` matches `query` via trigram similarity.
@@ -66,3 +67,28 @@ export function fts5SearchScoped(query, reader, sessionId, limit = 10) {
66
67
  // No session filter — plain FTS5 search across all sessions.
67
68
  return fts5Search(query, reader, limit);
68
69
  }
70
+ /**
71
+ * Hydrate FTS5 hits (which only carry checkpoint `id` + BM25 `score`, with NO
72
+ * checkpoint object) by joining against the real stored checkpoints. Mirrors the
73
+ * private `hydrateHits` in tieredRouter.ts (listCheckpoints → Map on
74
+ * checkpointId → join, dropping ids with no checkpoint).
75
+ *
76
+ * The joined `summary` is returned alongside the id because the 3WF-3 voter
77
+ * dedups candidates on the L0 CONTENT digest — hashing the id string instead
78
+ * would be a no-op tier (ids are already unique), silently disabling the check.
79
+ *
80
+ * @param hits Raw Fts5Hit rows (id + BM25 score).
81
+ * @param sessionId Session scope used to fetch the checkpoint set.
82
+ * @param stateDir On-disk state dir for the sync store.
83
+ */
84
+ export function hydrateFts5Hits(hits, sessionId, stateDir) {
85
+ const cps = listCheckpoints(sessionId, stateDir);
86
+ const cpMap = new Map(cps.map((cp) => [cp.checkpointId, cp]));
87
+ const out = [];
88
+ for (const h of hits) {
89
+ const cp = cpMap.get(h.id);
90
+ if (cp)
91
+ out.push({ checkpointId: h.id, score: h.score, summary: cp.summary });
92
+ }
93
+ return out;
94
+ }
@@ -241,6 +241,7 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
241
241
  num("MEGACOMPACT_L2_THRESHOLD", "L2 Cosine Threshold", "L2 semantic dedup firing point", 0.85, 0, 1),
242
242
  num("MEGACOMPACT_L1_JACCARD", "L1 Jaccard Threshold", "L1 MinHash near-dup threshold", 0.8, 0, 1),
243
243
  num("MEGACOMPACT_DEDUP_SIM", "Dedup Similarity", "Legacy content-similarity fallback", 0.9, 0, 1),
244
+ num("MEGACOMPACT_RECALL_MIN_COSINE", "Recall Min Cosine (same-repo)", "3WF-3 same-repo floor the 3-source validator applies to the top winner (cross-repo 0.90 stays separate)", 0.12, 0, 1),
244
245
  num("MEGACOMPACT_MMR_LAMBDA", "MMR Lambda", "Maximal Marginal Relevance diversity", 0.5, 0, 1),
245
246
  num("MEGACOMPACT_SEMDEDUP_COSINE", "SemDeDup Cosine", "Offline SemDeDup pair threshold", 0.95, 0, 1),
246
247
  num("MEGACOMPACT_CONSOLIDATE_COSINE", "Consolidate Cosine", "Memory consolidation merge threshold", 0.7, 0, 1),
@@ -119,6 +119,11 @@ export interface MegaConfig {
119
119
  * tighter than same-repo so only genuinely-relevant cross-repo context is
120
120
  * injected. */
121
121
  crossRepoCosine: number;
122
+ /** Same-repo recall cosine floor (3WF-3). Default 0.12. SEPARATE from
123
+ * `crossRepoCosine` (S17, default 0.90 — stricter, cross-repo only). The
124
+ * 3-source validator applies this to the top winner; hits below it are
125
+ * rejected in favor of the next-ranked candidate or the provenance floor. */
126
+ recallMinCosine: number;
122
127
  /** Memory-RAG auto-review enabled (S20). Every memoryReviewInterval turns the
123
128
  * conversation is auto-reviewed into durable add/replace/remove memories. */
124
129
  memoryAutoReview: boolean;
@@ -217,6 +217,12 @@ export function loadConfig(): MegaConfig {
217
217
  autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
218
218
  crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
219
219
  crossRepoCosine: Number(process.env.MEGACOMPACT_CROSSREPO_COSINE ?? "0.90"),
220
+ // 3WF-3: SAME-repo recall cosine floor applied by the 3-source validator to
221
+ // the top winner. SEPARATE from crossRepoCosine (S17, default 0.90, stricter
222
+ // and cross-repo only). This same-repo floor is permissive by default (0.12)
223
+ // so recall still surfaces loosely-relevant within-repo context while
224
+ // rejecting effectively-unrelated hits. Mirrors src/config.ts RECALL_MIN_COSINE.
225
+ recallMinCosine: Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12"),
220
226
  memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
221
227
  memoryReviewInterval: envFlag("MEGACOMPACT_MEMORY_REVIEW_INTERVAL", 10),
222
228
  recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),