pi-mega-compact 0.8.23 → 0.8.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
- package/dist/extensions/mega-compact-s38.test.js +263 -14
- package/dist/extensions/mega-compact.js +15 -0
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +211 -26
- package/dist/extensions/mega-events/context-handler.js +45 -7
- package/dist/extensions/mega-events/error-classifier.js +125 -18
- package/dist/extensions/mega-pipeline/compact.js +24 -13
- package/dist/extensions/mega-pipeline/recall.js +31 -2
- package/dist/extensions/mega-runtime/append-event.js +24 -0
- package/dist/extensions/mega-runtime/bind-repo.js +65 -0
- package/dist/extensions/mega-runtime/capture-model.js +87 -0
- package/dist/extensions/mega-runtime/dashboard-snapshot.js +122 -0
- package/dist/extensions/mega-runtime/effects.js +86 -0
- package/dist/extensions/mega-runtime/engine-view.js +11 -0
- package/dist/extensions/mega-runtime/game-state.js +116 -0
- package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
- package/dist/extensions/mega-runtime/perf.js +49 -0
- package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
- package/dist/extensions/mega-runtime/render-widget.js +17 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
- package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +208 -0
- package/dist/extensions/mega-runtime/runtime.js +405 -0
- package/dist/extensions/mega-runtime/snapshot.js +142 -0
- package/dist/extensions/mega-runtime/state.js +5 -1151
- package/dist/extensions/mega-runtime/status.js +11 -0
- package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
- package/dist/extensions/mega-runtime/widget-types.js +8 -0
- package/dist/extensions/mega-runtime/widget.js +15 -204
- package/dist/extensions/openclaw-mega-compact.js +291 -0
- package/dist/src/boundary.js +79 -43
- package/dist/src/boundary.test.js +119 -2
- package/dist/src/canary.js +10 -0
- package/dist/src/config/dedup.js +14 -0
- package/dist/src/config.js +3 -1
- package/dist/src/dedup/raptor/buildHistory.js +164 -0
- package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
- package/dist/src/dedup/raptor/index.js +38 -0
- package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
- package/dist/src/dedup/raptor/multilevel.js +17 -5
- package/dist/src/dedup/raptor/multilevel.test.js +36 -1
- package/dist/src/dedup/raptor/raptor.test.js +43 -0
- package/dist/src/dedup/raptor/retrieval.js +14 -2
- package/dist/src/dedup/raptor/retrieval.test.js +95 -0
- package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
- package/dist/src/dedup/raptor/summarizer.js +1 -0
- package/dist/src/dedup/raptor/tree.js +16 -2
- package/dist/src/engine.js +18 -2
- package/dist/src/httpEmbedder.js +96 -6
- package/dist/src/httpEmbedder.test.js +277 -0
- package/dist/src/mechanical-fix.test.js +65 -0
- package/dist/src/minilm.js +92 -0
- package/dist/src/raptor-inject-summaries.test.js +155 -0
- package/dist/src/recall.js +135 -21
- package/dist/src/recall.test.js +179 -4
- package/dist/src/store/sqlite/dedup-mirror.js +32 -15
- package/dist/src/store/sqlite/maintenance.js +2 -2
- package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
- package/dist/src/store/sqlite/memories.js +5 -5
- package/dist/src/store/sqlite/meta.js +1 -1
- package/dist/src/store/sqlite/raptor.js +56 -17
- package/dist/src/store/sqlite/raptor.test.js +106 -0
- package/dist/src/store/sqlite/schema.js +90 -1
- package/dist/src/store/sqlite/session-state.js +9 -3
- package/dist/src/store/sqlite/stats.js +9 -5
- package/dist/src/store/sqlite/turns.js +181 -0
- package/dist/src/store/sqlite/turns.test.js +183 -0
- package/dist/src/store/sqlite/utils.js +15 -4
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store.js +2 -2
- package/dist/src/vector-search-cache.test.js +157 -0
- package/dist/src/vector-search.js +107 -15
- package/dist/src/vectorStore.js +36 -8
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
- package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
- package/extensions/mega-compact-s38.test.ts +259 -14
- package/extensions/mega-compact.ts +15 -0
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-dashboard.ts +10 -1
- package/extensions/mega-events/agent-handlers.ts +211 -26
- package/extensions/mega-events/context-handler.ts +43 -7
- package/extensions/mega-events/error-classifier.ts +125 -17
- package/extensions/mega-pipeline/compact.ts +28 -16
- package/extensions/mega-pipeline/recall.ts +34 -2
- package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
- package/extensions/mega-runtime/README.md +38 -0
- package/extensions/mega-runtime/append-event.ts +40 -0
- package/extensions/mega-runtime/bind-repo.ts +81 -0
- package/extensions/mega-runtime/capture-model.ts +101 -0
- package/extensions/mega-runtime/dashboard-snapshot.ts +181 -0
- package/extensions/mega-runtime/effects.ts +129 -0
- package/extensions/mega-runtime/engine-view.ts +17 -0
- package/extensions/mega-runtime/game-state.ts +149 -0
- package/extensions/mega-runtime/get-state-dir.ts +19 -0
- package/extensions/mega-runtime/helpers.ts +25 -1
- package/extensions/mega-runtime/perf.ts +60 -0
- package/extensions/mega-runtime/pressure-getters.ts +96 -0
- package/extensions/mega-runtime/render-widget.ts +41 -0
- package/extensions/mega-runtime/runtime-helpers.ts +119 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +293 -0
- package/extensions/mega-runtime/runtime.ts +483 -0
- package/extensions/mega-runtime/snapshot.ts +230 -0
- package/extensions/mega-runtime/state.ts +5 -1268
- package/extensions/mega-runtime/status.ts +26 -0
- package/extensions/mega-runtime/widget-ansi.ts +217 -0
- package/extensions/mega-runtime/widget-types.ts +80 -0
- package/extensions/mega-runtime/widget.ts +34 -285
- package/package.json +1 -1
- package/src/boundary.test.ts +128 -2
- package/src/boundary.ts +75 -39
- package/src/canary.ts +10 -0
- package/src/config/dedup.ts +25 -0
- package/src/config.ts +3 -1
- package/src/dedup/raptor/buildHistory.test.ts +353 -0
- package/src/dedup/raptor/buildHistory.ts +259 -0
- package/src/dedup/raptor/index.ts +38 -0
- package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
- package/src/dedup/raptor/multilevel.test.ts +47 -0
- package/src/dedup/raptor/multilevel.ts +18 -8
- package/src/dedup/raptor/raptor.test.ts +59 -0
- package/src/dedup/raptor/retrieval.test.ts +118 -0
- package/src/dedup/raptor/retrieval.ts +14 -2
- package/src/dedup/raptor/serve-gate.test.ts +348 -0
- package/src/dedup/raptor/summarizer.ts +1 -0
- package/src/dedup/raptor/tree.ts +17 -2
- package/src/engine.ts +32 -3
- package/src/httpEmbedder.test.ts +286 -0
- package/src/httpEmbedder.ts +98 -8
- package/src/mechanical-fix.test.ts +70 -0
- package/src/raptor-inject-summaries.test.ts +212 -0
- package/src/recall.test.ts +220 -4
- package/src/recall.ts +151 -22
- package/src/store/sqlite/dedup-mirror.ts +35 -18
- package/src/store/sqlite/maintenance.ts +2 -2
- package/src/store/sqlite/mechanical-fix.test.ts +162 -0
- package/src/store/sqlite/memories.ts +5 -5
- package/src/store/sqlite/meta.ts +1 -1
- package/src/store/sqlite/raptor.test.ts +139 -0
- package/src/store/sqlite/raptor.ts +135 -81
- package/src/store/sqlite/schema.ts +90 -1
- package/src/store/sqlite/session-state.ts +9 -3
- package/src/store/sqlite/stats.ts +10 -8
- package/src/store/sqlite/turns.test.ts +218 -0
- package/src/store/sqlite/turns.ts +292 -0
- package/src/store/sqlite/utils.ts +14 -4
- package/src/store/sqlite.ts +1 -0
- package/src/store.ts +9 -2
- package/src/vector-search-cache.test.ts +190 -0
- package/src/vector-search.ts +273 -156
- package/src/vectorStore.ts +443 -382
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclaw-mega-compact — OpenClaw plugin adapter for the pi-mega-compact engine.
|
|
3
|
+
*
|
|
4
|
+
* Wires the pi-agnostic Trident engine (src/) into OpenClaw's plugin lifecycle:
|
|
5
|
+
* - Registers a CompactionProvider that replaces the built-in summarizeInStages.
|
|
6
|
+
* - Exposes `mega_status` and `mega_recall` tools for on-demand inspection.
|
|
7
|
+
* - Hooks into `before_compaction` / `after_compaction` for diagnostics.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints:
|
|
10
|
+
* - NO imports from `@earendil-works/pi-coding-agent` or pi-agent-core.
|
|
11
|
+
* - The engine core (src/) is pi-agnostic; this file is the sole OpenClaw boundary.
|
|
12
|
+
* - No network at runtime — everything is local (stores + extractive summarizer).
|
|
13
|
+
*/
|
|
14
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
15
|
+
import { compactSession, setDefaultStore, } from "../src/engine.js";
|
|
16
|
+
import { recallAndInline } from "../src/recall.js";
|
|
17
|
+
import { VectorStore } from "../src/vectorStore.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const PLUGIN_ID = "mega-compact";
|
|
22
|
+
const PLUGIN_LABEL = "Mega Compact (Trident)";
|
|
23
|
+
/** Default state directory for vector store persistence. */
|
|
24
|
+
const STATE_DIR = process.env.MEGA_COMPACT_STATE_DIR ?? undefined;
|
|
25
|
+
/** Minimum messages before we bother compacting. */
|
|
26
|
+
const MIN_MESSAGES_FOR_COMPACT = 6;
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Message conversion — OpenClaw unknown[] → EngineMessage[]
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Best-effort conversion from OpenClaw's opaque message array to our
|
|
32
|
+
* EngineMessage shape. OpenClaw messages are typed as `unknown[]` so we
|
|
33
|
+
* handle whatever shape comes through gracefully.
|
|
34
|
+
*/
|
|
35
|
+
function toEngineMessages(messages) {
|
|
36
|
+
return messages.map((msg) => {
|
|
37
|
+
if (!msg || typeof msg !== "object") {
|
|
38
|
+
// Primitive fallback — treat as custom text.
|
|
39
|
+
return {
|
|
40
|
+
role: "custom",
|
|
41
|
+
text: String(msg ?? ""),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const m = msg;
|
|
45
|
+
const role = typeof m.role === "string" ? m.role : "custom";
|
|
46
|
+
// Normalize role to one of our four engine roles.
|
|
47
|
+
let engineRole;
|
|
48
|
+
switch (role) {
|
|
49
|
+
case "user":
|
|
50
|
+
engineRole = "user";
|
|
51
|
+
break;
|
|
52
|
+
case "assistant":
|
|
53
|
+
engineRole = "assistant";
|
|
54
|
+
break;
|
|
55
|
+
case "tool":
|
|
56
|
+
case "function":
|
|
57
|
+
engineRole = "tool";
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
engineRole = "custom";
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
// Extract text content from common message shapes.
|
|
64
|
+
const text = typeof m.content === "string"
|
|
65
|
+
? m.content
|
|
66
|
+
: typeof m.text === "string"
|
|
67
|
+
? m.text
|
|
68
|
+
: Array.isArray(m.content)
|
|
69
|
+
? m.content
|
|
70
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
71
|
+
.map((part) => part.text)
|
|
72
|
+
.join("\n")
|
|
73
|
+
: "";
|
|
74
|
+
// Preserve tool metadata when present.
|
|
75
|
+
const toolName = typeof m.name === "string"
|
|
76
|
+
? m.name
|
|
77
|
+
: typeof m.toolName === "string"
|
|
78
|
+
? m.toolName
|
|
79
|
+
: undefined;
|
|
80
|
+
const input = typeof m.input === "string"
|
|
81
|
+
? m.input
|
|
82
|
+
: typeof m.arguments === "string"
|
|
83
|
+
? m.arguments
|
|
84
|
+
: m.arguments !== undefined
|
|
85
|
+
? JSON.stringify(m.arguments)
|
|
86
|
+
: undefined;
|
|
87
|
+
const output = typeof m.output === "string"
|
|
88
|
+
? m.output
|
|
89
|
+
: engineRole === "tool" && typeof m.content === "string"
|
|
90
|
+
? m.content
|
|
91
|
+
: undefined;
|
|
92
|
+
return { role: engineRole, text, toolName, input, output };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Compaction provider
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function createCompactionProvider(store) {
|
|
99
|
+
return {
|
|
100
|
+
id: PLUGIN_ID,
|
|
101
|
+
label: PLUGIN_LABEL,
|
|
102
|
+
async summarize({ messages, signal, compressionRatio, }) {
|
|
103
|
+
// Abort check — bail early if the caller cancelled.
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
throw new DOMException("Aborted", "AbortError");
|
|
106
|
+
}
|
|
107
|
+
const engineMessages = toEngineMessages(messages);
|
|
108
|
+
// Nothing meaningful to compact.
|
|
109
|
+
if (engineMessages.length < MIN_MESSAGES_FOR_COMPACT) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
// Map compression ratio → keepFrom boundary.
|
|
113
|
+
// compressionRatio=0.5 means "compact the oldest 50%".
|
|
114
|
+
// Default to compacting the oldest half if not specified.
|
|
115
|
+
const ratio = compressionRatio ?? 0.5;
|
|
116
|
+
const keepFrom = Math.max(MIN_MESSAGES_FOR_COMPACT, Math.floor(engineMessages.length * (1 - ratio)));
|
|
117
|
+
// Abort check after conversion (conversion is cheap but check anyway).
|
|
118
|
+
if (signal?.aborted) {
|
|
119
|
+
throw new DOMException("Aborted", "AbortError");
|
|
120
|
+
}
|
|
121
|
+
const sessionId = `openclaw-${Date.now()}`;
|
|
122
|
+
const input = {
|
|
123
|
+
sessionId,
|
|
124
|
+
messages: engineMessages,
|
|
125
|
+
keepFrom,
|
|
126
|
+
};
|
|
127
|
+
const result = compactSession(input, store);
|
|
128
|
+
if (result.skipped) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
return result.summary;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Plugin entry
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export default definePluginEntry({
|
|
139
|
+
id: PLUGIN_ID,
|
|
140
|
+
name: "Mega Compact",
|
|
141
|
+
description: "Layered, local, vector-backed context compressor (Trident engine) for OpenClaw compaction.",
|
|
142
|
+
register(api) {
|
|
143
|
+
const logger = api.logger;
|
|
144
|
+
// Resolve state directory — prefer plugin config override.
|
|
145
|
+
const pluginCfg = (api.pluginConfig ?? {});
|
|
146
|
+
const stateDir = typeof pluginCfg.stateDir === "string" && pluginCfg.stateDir.length > 0
|
|
147
|
+
? pluginCfg.stateDir
|
|
148
|
+
: STATE_DIR;
|
|
149
|
+
// Initialize vector store.
|
|
150
|
+
let store;
|
|
151
|
+
try {
|
|
152
|
+
store = new VectorStore({ stateDir });
|
|
153
|
+
setDefaultStore(store);
|
|
154
|
+
logger.info?.(`${PLUGIN_ID}: vector store initialized (stateDir=${stateDir ?? "default"})`);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
logger.error?.(`${PLUGIN_ID}: failed to init vector store:`, err);
|
|
158
|
+
return; // Hard bail — no point registering if store is broken.
|
|
159
|
+
}
|
|
160
|
+
// -----------------------------------------------------------------------
|
|
161
|
+
// Register compaction provider
|
|
162
|
+
// -----------------------------------------------------------------------
|
|
163
|
+
const provider = createCompactionProvider(store);
|
|
164
|
+
api.registerCompactionProvider(provider);
|
|
165
|
+
logger.info?.(`${PLUGIN_ID}: registered compaction provider "${provider.id}"`);
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
// Hooks — before / after compaction diagnostics
|
|
168
|
+
// -----------------------------------------------------------------------
|
|
169
|
+
api.registerHook({
|
|
170
|
+
event: "before_compaction",
|
|
171
|
+
handler: async (ctx) => {
|
|
172
|
+
const msgCount = Array.isArray(ctx?.messages) ? ctx.messages.length : 0;
|
|
173
|
+
logger.info?.(`${PLUGIN_ID}: before_compaction — ${msgCount} messages in scope`);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
api.registerHook({
|
|
177
|
+
event: "after_compaction",
|
|
178
|
+
handler: async (ctx) => {
|
|
179
|
+
const summaryLen = typeof ctx?.summary === "string" ? ctx.summary.length : 0;
|
|
180
|
+
logger.info?.(`${PLUGIN_ID}: after_compaction — summary ${summaryLen} chars`);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
// Tool: mega_status
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
api.registerTool({
|
|
187
|
+
name: "mega_status",
|
|
188
|
+
description: "Show the current status of the mega-compact engine: vector store stats, checkpoint count, and recent compaction activity.",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
sessionId: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "Optional session ID to scope stats to.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
},
|
|
199
|
+
handler: async (args) => {
|
|
200
|
+
const sessionId = args?.sessionId ?? "global";
|
|
201
|
+
try {
|
|
202
|
+
const stats = store.stats(sessionId);
|
|
203
|
+
const parts = [
|
|
204
|
+
`**Mega Compact Status**`,
|
|
205
|
+
`Session: ${sessionId}`,
|
|
206
|
+
`Checkpoints: ${stats.checkpointCount}`,
|
|
207
|
+
`Total tokens saved: ${stats.totalTokenEstimate}`,
|
|
208
|
+
`Last checkpoint: ${stats.lastCheckpointId ?? "—"}`,
|
|
209
|
+
`Injected count: ${stats.injectedCount}`,
|
|
210
|
+
`Dedup hit rate: ${(stats.dedupHitRate * 100).toFixed(0)}%`,
|
|
211
|
+
];
|
|
212
|
+
if (stats.lastSummary) {
|
|
213
|
+
parts.push(`\nLast summary (truncated):\n ${stats.lastSummary.slice(0, 120).replace(/\n/g, " ")}…`);
|
|
214
|
+
}
|
|
215
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text", text: `Error reading mega-compact status: ${err}` }],
|
|
220
|
+
isError: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// Tool: mega_recall
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
api.registerTool({
|
|
229
|
+
name: "mega_recall",
|
|
230
|
+
description: "Recall and inline relevant context from the mega-compact vector store for the current session.",
|
|
231
|
+
parameters: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
sessionId: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "Session ID to recall context for.",
|
|
237
|
+
},
|
|
238
|
+
query: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Natural language query for relevant context.",
|
|
241
|
+
},
|
|
242
|
+
limit: {
|
|
243
|
+
type: "number",
|
|
244
|
+
description: "Max checkpoints to recall (default 3).",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
required: ["sessionId", "query"],
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
},
|
|
250
|
+
handler: async (args) => {
|
|
251
|
+
const { sessionId, query, limit } = args;
|
|
252
|
+
if (!sessionId || !query) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: "Both `sessionId` and `query` are required." }],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const result = recallAndInline({ sessionId, query, limit: limit ?? 3, source: "command", skipInjected: false }, store);
|
|
260
|
+
if (result.toInject.length === 0) {
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text", text: "No relevant context found in the mega-compact store." }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const parts = [
|
|
266
|
+
`**Recalled ${result.toInject.length} checkpoint(s):**`,
|
|
267
|
+
...result.report,
|
|
268
|
+
"",
|
|
269
|
+
"---",
|
|
270
|
+
result.block,
|
|
271
|
+
];
|
|
272
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
content: [{ type: "text", text: `Error during mega-recall: ${err}` }],
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
// -----------------------------------------------------------------------
|
|
283
|
+
// Cleanup on shutdown
|
|
284
|
+
// -----------------------------------------------------------------------
|
|
285
|
+
api.on("shutdown", () => {
|
|
286
|
+
logger.info?.(`${PLUGIN_ID}: shutting down — clearing default store`);
|
|
287
|
+
setDefaultStore(undefined);
|
|
288
|
+
});
|
|
289
|
+
logger.info?.(`${PLUGIN_ID}: plugin registered (tools: mega_status, mega_recall)`);
|
|
290
|
+
},
|
|
291
|
+
});
|
package/dist/src/boundary.js
CHANGED
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
* 1. ANCHOR FLOOR: never drop the most recent N user messages.
|
|
6
6
|
* 2. TOOL-PAIR: never split an assistant(toolCall) from its following
|
|
7
7
|
* tool-result message — an orphaned `tool` role with no preceding
|
|
8
|
-
* assistant tool call causes a 400 on the OpenAI-compat path.
|
|
8
|
+
* assistant tool call causes a 400 on the OpenAI-compat path. The pair
|
|
9
|
+
* invariant outranks the anchor floor: on conflict we drop LESS (lower the
|
|
10
|
+
* drop end), never cross a pair.
|
|
9
11
|
*
|
|
10
12
|
* The engine reasons over EngineMessage; the pi adapter maps role "tool" +
|
|
11
|
-
* toolName to the tool-result shape.
|
|
13
|
+
* toolName to the tool-result shape. EngineMessage carries no tool-call id, so
|
|
14
|
+
* ownership is positional: a tool result's owner is its nearest preceding
|
|
15
|
+
* assistant tool-call (the last assistant message with a `toolName` before it).
|
|
16
|
+
* A preserved tool result is orphaned by a cut when its owner is dropped; the
|
|
17
|
+
* guard rejects any cut that drops an owner while preserving its result, for
|
|
18
|
+
* ARBITRARY interleavings (custom/non-tool messages between call and result,
|
|
19
|
+
* consecutive results sharing one call, a cut landing directly on a call whose
|
|
20
|
+
* results follow).
|
|
12
21
|
*/
|
|
13
22
|
/** Is this message a tool result (pi `tool` role with a tool name)? */
|
|
14
23
|
function isToolResult(m) {
|
|
@@ -18,16 +27,56 @@ function isToolResult(m) {
|
|
|
18
27
|
function hasToolUse(m) {
|
|
19
28
|
return Boolean(m.toolName) && m.role !== "tool";
|
|
20
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Is the drop boundary at `dropEnd` pair-safe? The preserved run is
|
|
32
|
+
* [dropEnd, messages.length). The cut is pair-safe iff NO preserved tool result
|
|
33
|
+
* is orphaned: for every tool result at index >= dropEnd, its nearest preceding
|
|
34
|
+
* assistant tool-call must EXIST and be PRESERVED (index >= dropEnd). A tool
|
|
35
|
+
* result with no preceding assistant tool-call is already orphaned in the
|
|
36
|
+
* input — we treat that as unsafe too, so the guard never endorses shipping an
|
|
37
|
+
* orphaned result to the provider.
|
|
38
|
+
*
|
|
39
|
+
* O(messages.length) single forward pass; early-exits on the first orphan. The
|
|
40
|
+
* owner of each result is the most recent `hasToolUse` message seen so far
|
|
41
|
+
* (tracked across the whole stream, including dropped messages, because a
|
|
42
|
+
* dropped assistant tool-call is exactly the owner we must reject).
|
|
43
|
+
*/
|
|
44
|
+
export function isPairSafe(messages, dropEnd) {
|
|
45
|
+
if (dropEnd <= 0 || dropEnd >= messages.length)
|
|
46
|
+
return true;
|
|
47
|
+
let lastToolCall = -1;
|
|
48
|
+
for (let i = 0; i < messages.length; i++) {
|
|
49
|
+
if (hasToolUse(messages[i]))
|
|
50
|
+
lastToolCall = i;
|
|
51
|
+
if (i >= dropEnd && isToolResult(messages[i])) {
|
|
52
|
+
if (lastToolCall === -1)
|
|
53
|
+
return false; // no preceding call → orphaned
|
|
54
|
+
if (lastToolCall < dropEnd)
|
|
55
|
+
return false; // owner dropped → orphaned
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
21
60
|
/**
|
|
22
61
|
* Compute the safe drop range [dropStart, dropEnd) within `messages`.
|
|
23
|
-
* `keepFrom` is the caller's desired first-preserved index. We then:
|
|
24
|
-
* 1. Walk it back (lower dropEnd = keep more) so the first preserved message
|
|
25
|
-
* is never an orphaned tool result (tool-pair invariant).
|
|
26
|
-
* 2. Raise it (lower dropEnd) to the anchor floor so the last N user messages
|
|
27
|
-
* are never dropped, when enough user messages exist.
|
|
28
62
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
63
|
+
* Contract:
|
|
64
|
+
* - `keepFrom` is the caller's desired first-preserved index (drop [0, keepFrom)).
|
|
65
|
+
* - `dropEnd` is the first index KEPT; we may LOWER it (keep more) to satisfy the
|
|
66
|
+
* guards, never raise it above keepFrom.
|
|
67
|
+
* - The anchor floor (PREVENT-PI-001) caps dropEnd at the index of the
|
|
68
|
+
* Nth-from-last user message so the last N user messages are never dropped.
|
|
69
|
+
* - The tool-pair invariant (PREVENT-PI-002) rejects any dropEnd that orphans a
|
|
70
|
+
* preserved tool result; on conflict with the anchor floor the pair rule wins
|
|
71
|
+
* (we drop less, never cross a pair).
|
|
72
|
+
* - We return the LARGEST pair-safe dropEnd <= min(keepFrom, anchorStart) so the
|
|
73
|
+
* caller drops as much as is safe. When no pair-safe positive cut exists at
|
|
74
|
+
* or below keepFrom, we return [0, 0] (no-op) — the pair rule outranks
|
|
75
|
+
* dropping. dropStart is always 0 today (we drop a prefix); reserved for
|
|
76
|
+
* future two-sided trimming.
|
|
77
|
+
*
|
|
78
|
+
* Returns [0, 0] (empty range, drop nothing) when keepFrom is out of range or no
|
|
79
|
+
* pair-safe positive cut exists.
|
|
31
80
|
*/
|
|
32
81
|
export function computeDropRange(messages, keepFrom, anchorUserMessages) {
|
|
33
82
|
if (keepFrom <= 0 || keepFrom >= messages.length)
|
|
@@ -36,49 +85,36 @@ export function computeDropRange(messages, keepFrom, anchorUserMessages) {
|
|
|
36
85
|
messages.forEach((m, i) => { if (m.role === "user")
|
|
37
86
|
userIndexes.push(i); });
|
|
38
87
|
const anchorActive = anchorUserMessages > 0 && userIndexes.length >= anchorUserMessages;
|
|
39
|
-
const anchorStart = anchorActive ? userIndexes[userIndexes.length - anchorUserMessages] :
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
k -= 1; // pair intact across boundary — include the assistant turn
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
k -= 1;
|
|
88
|
+
const anchorStart = anchorActive ? userIndexes[userIndexes.length - anchorUserMessages] : keepFrom;
|
|
89
|
+
// Upper bound on dropEnd: never keep less than the caller asked (dropEnd <= keepFrom)
|
|
90
|
+
// and never drop a must-keep user message (dropEnd <= anchorStart).
|
|
91
|
+
const upperBound = Math.min(keepFrom, anchorActive ? anchorStart : keepFrom);
|
|
92
|
+
// Walk down from the upper bound to find the largest pair-safe cut. dropEnd=0
|
|
93
|
+
// (drop nothing) is always pair-safe; the loop finds the largest positive cut,
|
|
94
|
+
// and falls back to [0, 0] when none exists — the pair rule outranks dropping.
|
|
95
|
+
for (let dropEnd = upperBound; dropEnd > 0; dropEnd--) {
|
|
96
|
+
if (isPairSafe(messages, dropEnd))
|
|
97
|
+
return [0, dropEnd];
|
|
53
98
|
}
|
|
54
|
-
|
|
55
|
-
k = floor;
|
|
56
|
-
// Anchor floor: never drop a must-keep user message. Raise dropEnd so we keep
|
|
57
|
-
// from anchorStart onward when the walk didn't already.
|
|
58
|
-
if (anchorActive && k > anchorStart)
|
|
59
|
-
k = anchorStart;
|
|
60
|
-
if (k <= 0)
|
|
61
|
-
return [0, 0];
|
|
62
|
-
return [0, k];
|
|
99
|
+
return [0, 0];
|
|
63
100
|
}
|
|
64
101
|
/**
|
|
65
102
|
* Validate that the intended split at `keepFrom` (drop [0, keepFrom), keep the
|
|
66
|
-
* rest) does not
|
|
67
|
-
*
|
|
68
|
-
*
|
|
103
|
+
* rest) does not orphan any preserved tool result. Checks the FULL preserved
|
|
104
|
+
* run, not just the first message, so it holds for arbitrary interleavings
|
|
105
|
+
* (custom messages between call and result, consecutive shared-call results, a
|
|
106
|
+
* cut landing on a call whose results follow). Used on the every-LLM-call
|
|
107
|
+
* live-trim hot path (extensions/mega-trim.ts) and by dropCompactedRange
|
|
108
|
+
* (src/adapt.ts).
|
|
69
109
|
*/
|
|
70
110
|
export function isBoundarySafe(messages, keepFrom) {
|
|
71
|
-
|
|
72
|
-
return true;
|
|
73
|
-
const firstPreserved = messages[keepFrom];
|
|
74
|
-
if (!isToolResult(firstPreserved))
|
|
75
|
-
return true;
|
|
76
|
-
const preceding = messages[keepFrom - 1];
|
|
77
|
-
return Boolean(preceding && hasToolUse(preceding));
|
|
111
|
+
return isPairSafe(messages, keepFrom);
|
|
78
112
|
}
|
|
79
113
|
/**
|
|
80
114
|
* Drop everything before the safe keep-index, honoring both guards, returning
|
|
81
|
-
* the filtered message list.
|
|
115
|
+
* the filtered message list. Returns the original array reference (unchanged)
|
|
116
|
+
* when the safe range is empty so callers can short-circuit on reference
|
|
117
|
+
* equality.
|
|
82
118
|
*/
|
|
83
119
|
export function dropBefore(messages, keepFrom, anchorUserMessages) {
|
|
84
120
|
const [dropStart, dropEnd] = computeDropRange(messages, keepFrom, anchorUserMessages);
|
|
@@ -5,6 +5,7 @@ function user(t) { return { role: "user", text: t }; }
|
|
|
5
5
|
function assistant(t) { return { role: "assistant", text: t }; }
|
|
6
6
|
function toolUse(n, i = "{}") { return { role: "assistant", text: "", toolName: n, input: i }; }
|
|
7
7
|
function toolResult(n, o = "ok") { return { role: "tool", text: "", toolName: n, output: o }; }
|
|
8
|
+
function custom(t) { return { role: "custom", text: t }; }
|
|
8
9
|
test("walks back so first preserved message is not an orphaned tool result", () => {
|
|
9
10
|
const messages = [
|
|
10
11
|
user("Search for files"),
|
|
@@ -21,9 +22,14 @@ test("walks back so first preserved message is not an orphaned tool result", ()
|
|
|
21
22
|
assert.notEqual(kept[0].role, "tool");
|
|
22
23
|
assert.equal(kept[0].toolName, "search"); // assistant tool-call preserved
|
|
23
24
|
});
|
|
24
|
-
test("isBoundarySafe:
|
|
25
|
+
test("isBoundarySafe: cut that drops a toolCall but keeps its toolResult is unsafe (PREVENT-PI-002)", () => {
|
|
25
26
|
const messages = [user("a"), toolUse("search"), toolResult("search")];
|
|
26
|
-
|
|
27
|
+
// keepFrom=2 drops the toolUse at index 1 but keeps the toolResult at index 2 →
|
|
28
|
+
// the preserved run starts on an orphaned tool result. This is the shape the old
|
|
29
|
+
// check mis-validated (it only compared messages[keepFrom] to messages[keepFrom-1]).
|
|
30
|
+
assert.equal(isBoundarySafe(messages, 2), false);
|
|
31
|
+
// keepFrom=1 keeps the toolCall together with its toolResult → safe.
|
|
32
|
+
assert.equal(isBoundarySafe(messages, 1), true);
|
|
27
33
|
});
|
|
28
34
|
test("isBoundarySafe: orphaned tool result without preceding tool use is unsafe", () => {
|
|
29
35
|
const messages = [user("a"), toolResult("search", "orphan")];
|
|
@@ -51,3 +57,114 @@ test("dropBefore returns original when range is empty", () => {
|
|
|
51
57
|
const messages = [user("a"), assistant("b")];
|
|
52
58
|
assert.equal(dropBefore(messages, 0, 1), messages);
|
|
53
59
|
});
|
|
60
|
+
// --- PREVENT-PI-002 regression cases for arbitrary interleavings ---
|
|
61
|
+
test("interleaved custom message between toolCall and toolResult: walk-back keeps the call", () => {
|
|
62
|
+
// [user, assistant(tc=read), custom/bashExecution, tool(read-result)] with
|
|
63
|
+
// desired keepFrom=3. The old walk-back saw messages[2] was not a toolUse and
|
|
64
|
+
// broke at k=2, dropping the assistant toolCall at index 1 while KEEPING its
|
|
65
|
+
// tool result at index 3 → orphaned tool result → provider 400.
|
|
66
|
+
const messages = [
|
|
67
|
+
user("Search for files"),
|
|
68
|
+
toolUse("read"),
|
|
69
|
+
custom("bash: ls -la"),
|
|
70
|
+
toolResult("read", "file contents"),
|
|
71
|
+
assistant("Done."),
|
|
72
|
+
];
|
|
73
|
+
const [start, end] = computeDropRange(messages, 3, 0);
|
|
74
|
+
assert.equal(start, 0);
|
|
75
|
+
assert.equal(end, 1); // keep the assistant toolCall at index 1 with its result
|
|
76
|
+
const kept = messages.slice(end);
|
|
77
|
+
assert.equal(kept[0].toolName, "read"); // assistant tool-call preserved
|
|
78
|
+
assert.ok(kept.some((m) => m.role === "tool" && m.toolName === "read"));
|
|
79
|
+
// The toolCall and its toolResult are both in the kept run.
|
|
80
|
+
const callIdx = kept.findIndex((m) => m.role === "assistant" && m.toolName === "read");
|
|
81
|
+
const resultIdx = kept.findIndex((m) => m.role === "tool" && m.toolName === "read");
|
|
82
|
+
assert.ok(callIdx !== -1 && resultIdx !== -1 && callIdx < resultIdx);
|
|
83
|
+
});
|
|
84
|
+
test("consecutive tool results sharing one call: no-op when the call cannot be kept", () => {
|
|
85
|
+
// [assistant(tc), T1, T2] with keepFrom=1: dropping the call orphans BOTH T1
|
|
86
|
+
// and T2. No pair-safe positive cut exists below keepFrom → no-op compaction
|
|
87
|
+
// (the [start,end) contract preserves a non-zero result only when one exists).
|
|
88
|
+
const messages = [toolUse("multi"), toolResult("multi", "r1"), toolResult("multi", "r2")];
|
|
89
|
+
const [start, end] = computeDropRange(messages, 1, 0);
|
|
90
|
+
assert.equal(start, 0);
|
|
91
|
+
assert.equal(end, 0); // no-op — pair rule outranks dropping
|
|
92
|
+
assert.equal(dropBefore(messages, 1, 0), messages);
|
|
93
|
+
});
|
|
94
|
+
test("consecutive tool results sharing one call: safe cut keeps the call with both results", () => {
|
|
95
|
+
// [user, assistant(tc), T1, T2, user2] — keepFrom=3 would orphan T2; the guard
|
|
96
|
+
// walks back to keep the call (dropEnd=1, only the first user is dropped).
|
|
97
|
+
const messages = [
|
|
98
|
+
user("u1"),
|
|
99
|
+
toolUse("multi"),
|
|
100
|
+
toolResult("multi", "r1"),
|
|
101
|
+
toolResult("multi", "r2"),
|
|
102
|
+
user("u2"),
|
|
103
|
+
];
|
|
104
|
+
const [start, end] = computeDropRange(messages, 3, 0);
|
|
105
|
+
assert.equal(start, 0);
|
|
106
|
+
assert.equal(end, 1); // keep [assistant(tc), T1, T2, user2]
|
|
107
|
+
const kept = messages.slice(end);
|
|
108
|
+
const callIdx = kept.findIndex((m) => m.role === "assistant" && m.toolName === "multi");
|
|
109
|
+
const r1Idx = kept.findIndex((m) => m.role === "tool" && m.output === "r1");
|
|
110
|
+
const r2Idx = kept.findIndex((m) => m.role === "tool" && m.output === "r2");
|
|
111
|
+
assert.ok(callIdx < r1Idx && r1Idx < r2Idx);
|
|
112
|
+
});
|
|
113
|
+
test("consecutive shared-call results: dropping call + all results together is safe", () => {
|
|
114
|
+
// keepFrom=4 preserves only the trailing user — the call and BOTH results are
|
|
115
|
+
// dropped together, so nothing is orphaned.
|
|
116
|
+
const messages = [
|
|
117
|
+
user("u1"),
|
|
118
|
+
toolUse("multi"),
|
|
119
|
+
toolResult("multi", "r1"),
|
|
120
|
+
toolResult("multi", "r2"),
|
|
121
|
+
user("u2"),
|
|
122
|
+
];
|
|
123
|
+
const [start, end] = computeDropRange(messages, 4, 0);
|
|
124
|
+
assert.equal(start, 0);
|
|
125
|
+
assert.equal(end, 4);
|
|
126
|
+
assert.deepEqual(messages.slice(end), [user("u2")]);
|
|
127
|
+
});
|
|
128
|
+
test("keepFrom landing on a call whose results follow is safe", () => {
|
|
129
|
+
// [user, assistant(tc1), T1, assistant(tc2), T2, user2] keepFrom=3 → preserved
|
|
130
|
+
// run starts on assistant(tc2) at index 3, whose result T2 follows. Safe.
|
|
131
|
+
const messages = [
|
|
132
|
+
user("u1"),
|
|
133
|
+
toolUse("read"),
|
|
134
|
+
toolResult("read", "r1"),
|
|
135
|
+
toolUse("write"),
|
|
136
|
+
toolResult("write", "r2"),
|
|
137
|
+
user("u2"),
|
|
138
|
+
];
|
|
139
|
+
const [start, end] = computeDropRange(messages, 3, 0);
|
|
140
|
+
assert.equal(start, 0);
|
|
141
|
+
assert.equal(end, 3);
|
|
142
|
+
const kept = messages.slice(end);
|
|
143
|
+
assert.equal(kept[0].role, "assistant");
|
|
144
|
+
assert.equal(kept[0].toolName, "write");
|
|
145
|
+
assert.ok(kept.some((m) => m.role === "tool" && m.toolName === "write"));
|
|
146
|
+
});
|
|
147
|
+
test("anchor floor + pair-rule conflict: pair rule wins, drop less", () => {
|
|
148
|
+
// [assistant(tc), user, T] with anchor=1: the anchor floor wants dropEnd<=1
|
|
149
|
+
// (keep the user at index 1), but keeping from index 1 orphans T (its owner at
|
|
150
|
+
// index 0 would be dropped). The pair rule outranks the floor — we drop LESS,
|
|
151
|
+
// keeping everything (no-op) rather than cross a pair.
|
|
152
|
+
const messages = [toolUse("read"), user("keep me"), toolResult("read", "r")];
|
|
153
|
+
const out = dropBefore(messages, 2, 1);
|
|
154
|
+
assert.equal(out, messages, "anchor floor would orphan the tool result → no-op");
|
|
155
|
+
});
|
|
156
|
+
test("isBoundarySafe: interleaved custom between call and result is detected unsafe", () => {
|
|
157
|
+
const messages = [user("a"), toolUse("read"), custom("bash"), toolResult("read", "r")];
|
|
158
|
+
// keepFrom=3 drops the call at index 1, keeps the result at index 3 → unsafe.
|
|
159
|
+
assert.equal(isBoundarySafe(messages, 3), false);
|
|
160
|
+
// keepFrom=2 ALSO drops the call at index 1 (drop [0,2) = [user, toolUse]) and
|
|
161
|
+
// keeps the result at index 3 → still orphaned → unsafe.
|
|
162
|
+
assert.equal(isBoundarySafe(messages, 2), false);
|
|
163
|
+
// keepFrom=1 keeps the call (index 1) together with its result at index 3 → safe.
|
|
164
|
+
assert.equal(isBoundarySafe(messages, 1), true);
|
|
165
|
+
});
|
|
166
|
+
test("isBoundarySafe: cut before any tool result is safe", () => {
|
|
167
|
+
const messages = [user("a"), toolUse("read"), toolResult("read", "r"), assistant("done")];
|
|
168
|
+
assert.equal(isBoundarySafe(messages, 0), true); // out of range → safe
|
|
169
|
+
assert.equal(isBoundarySafe(messages, messages.length), true); // out of range → safe
|
|
170
|
+
});
|
package/dist/src/canary.js
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* The controller owns a MUTABLE working copy of the dedup config; callers read
|
|
9
9
|
* `controller.config` after each step. Tiers disabled via MARK_ONLY degrade
|
|
10
10
|
* gracefully rather than fully off.
|
|
11
|
+
*
|
|
12
|
+
* Persistence design: disabled state is IN-MEMORY ONLY by design. The dedup
|
|
13
|
+
* config loads from MEGACOMPACT_* env vars (config/dedup.ts) with no durable
|
|
14
|
+
* save mechanism. `setEnabled` DOES mutate `this.config.L1_ENABLED` etc. so
|
|
15
|
+
* callers reading `controller.config` see the disabled state for the current
|
|
16
|
+
* session. On restart, env defaults re-apply and the canary sequences again
|
|
17
|
+
* from L0 — this is intentional: a tier disabled due to a cold cache or
|
|
18
|
+
* transient load gets a fresh evaluation each run rather than being locked
|
|
19
|
+
* out forever. To permanently disable a tier, set the corresponding
|
|
20
|
+
* MEGACOMPACT_*_ENABLED env var to false.
|
|
11
21
|
*/
|
|
12
22
|
import { loadDedupConfig } from "./config/dedup.js";
|
|
13
23
|
import { p95 } from "./monitoring.js";
|
package/dist/src/config/dedup.js
CHANGED
|
@@ -27,6 +27,14 @@ function envNum(name, def) {
|
|
|
27
27
|
const n = Number(v);
|
|
28
28
|
return Number.isFinite(n) ? n : def;
|
|
29
29
|
}
|
|
30
|
+
/** S42B: parse a comma-separated numeric env var (e.g. "1.0,0.9,0.8"). */
|
|
31
|
+
function envNumArray(name, def) {
|
|
32
|
+
const v = process.env[name];
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
return def;
|
|
35
|
+
const parts = v.split(",").map((x) => Number(x.trim()));
|
|
36
|
+
return parts.length > 0 && parts.every((n) => Number.isFinite(n)) ? parts : def;
|
|
37
|
+
}
|
|
30
38
|
/** Read the current dedup config from env (file defaults reproduce Sprint 13). */
|
|
31
39
|
export function loadDedupConfig() {
|
|
32
40
|
return {
|
|
@@ -53,6 +61,12 @@ export function loadDedupConfig() {
|
|
|
53
61
|
RAPTOR_BUDGET_MS: envNum("MEGACOMPACT_RAPTOR_BUDGET_MS", 5000),
|
|
54
62
|
RAPTOR_CLUSTERS_PER_LEVEL: envNum("MEGACOMPACT_RAPTOR_CLUSTERS", 5),
|
|
55
63
|
RAPTOR_CONSISTENCY: envNum("MEGACOMPACT_RAPTOR_CONSISTENCY", 0.6),
|
|
64
|
+
RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
|
|
65
|
+
RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
|
|
66
|
+
RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
|
|
67
|
+
RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
|
|
68
|
+
RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
|
|
69
|
+
RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),
|
|
56
70
|
FP_RATE_L0: envNum("MEGACOMPACT_FP_RATE_L0", 0.01),
|
|
57
71
|
FP_RATE_L1L2: envNum("MEGACOMPACT_FP_RATE_L1L2", 0.05),
|
|
58
72
|
ALERT_WINDOW_MS: envNum("MEGACOMPACT_ALERT_WINDOW_MS", 600_000),
|
package/dist/src/config.js
CHANGED
|
@@ -29,7 +29,9 @@ export function pressureFromPct(pct) {
|
|
|
29
29
|
export function preserveRecentForPressure(pressure, preserveRecent, preserveRecentMin) {
|
|
30
30
|
const p = pressure < 0 ? 0 : pressure > 1 ? 1 : pressure;
|
|
31
31
|
const v = Math.round(preserveRecent - (preserveRecent - preserveRecentMin) * p);
|
|
32
|
-
|
|
32
|
+
// Floor of 1: even with preserveRecentMin=0 at full pressure, never compact
|
|
33
|
+
// ALL messages — the boundary guard (computeDropRange) needs ≥1 to anchor on.
|
|
34
|
+
return Math.max(1, preserveRecentMin, Math.min(preserveRecent, v));
|
|
33
35
|
}
|
|
34
36
|
/** Clamp a pressure ratio into [0, 1]. */
|
|
35
37
|
function clamp01(p) {
|