pi-mega-compact 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/vector-cortex.js +19 -0
- package/dist/config.js +117 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +4 -0
- package/dist/extensions/mega-events/context-handler/dbMirrorAppend.js +63 -0
- package/dist/extensions/mega-events/context-handler/gateCheck.js +59 -0
- package/dist/extensions/mega-events/context-handler/liveTrim.js +178 -0
- package/dist/extensions/mega-events/context-handler/pipelineRun.js +37 -0
- package/dist/extensions/mega-events/context-handler.js +39 -305
- package/dist/log.js +47 -0
- package/dist/src/config/vector-cortex.js +19 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/encoder/asset.js +142 -0
- package/dist/src/vector-cortex/encoder/emit-vc2b.js +63 -0
- package/dist/src/vector-cortex/encoder/emit.js +42 -0
- package/dist/src/vector-cortex/encoder/heads.js +113 -0
- package/dist/src/vector-cortex/encoder/lexical.js +104 -0
- package/dist/src/vector-cortex/encoder/router.js +115 -0
- package/dist/src/vector-cortex/encoder/runtime.js +228 -0
- package/dist/src/vector-cortex/encoder/trigram.js +75 -0
- package/dist/src/vector-cortex/encoder/types.js +138 -0
- package/dist/vector-cortex/encoder/asset.js +142 -0
- package/dist/vector-cortex/encoder/emit-vc2b.js +63 -0
- package/dist/vector-cortex/encoder/emit.js +42 -0
- package/dist/vector-cortex/encoder/heads.js +113 -0
- package/dist/vector-cortex/encoder/lexical.js +104 -0
- package/dist/vector-cortex/encoder/router.js +115 -0
- package/dist/vector-cortex/encoder/runtime.js +228 -0
- package/dist/vector-cortex/encoder/trigram.js +75 -0
- package/dist/vector-cortex/encoder/types.js +138 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +14 -0
- package/extensions/mega-events/context-handler/dbMirrorAppend.ts +93 -0
- package/extensions/mega-events/context-handler/gateCheck.ts +101 -0
- package/extensions/mega-events/context-handler/liveTrim.ts +241 -0
- package/extensions/mega-events/context-handler/pipelineRun.ts +79 -0
- package/extensions/mega-events/context-handler.ts +45 -347
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +21 -0
- package/src/config.ts +2 -0
- package/src/vector-cortex/encoder/asset.ts +155 -0
- package/src/vector-cortex/encoder/emit-vc2b.ts +82 -0
- package/src/vector-cortex/encoder/emit.ts +51 -0
- package/src/vector-cortex/encoder/heads.ts +142 -0
- package/src/vector-cortex/encoder/lexical.ts +123 -0
- package/src/vector-cortex/encoder/router.ts +163 -0
- package/src/vector-cortex/encoder/runtime.ts +283 -0
- package/src/vector-cortex/encoder/trigram.ts +85 -0
- package/src/vector-cortex/encoder/types.ts +275 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { estimateSessionTokens, estimateBlockTokens, estimateMessageTokens, } from "../../src/tokens.js";
|
|
4
|
-
import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
|
|
5
|
-
import { pressureFromPct, pressureRatio, } from "../mega-config.js";
|
|
6
|
-
import { appendMirrorMessages } from "./mirror-append.js";
|
|
7
|
-
import { appendMessagesToLedger } from "../mega-runtime/vector-cortex-ledger.js";
|
|
8
|
-
import { epochIdFor } from "../../src/mirror/epoch.js";
|
|
9
|
-
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
|
|
10
|
-
import { messageContentText } from "./context-handler/messageText.js";
|
|
1
|
+
import { estimateSessionTokens } from "../../src/tokens.js";
|
|
2
|
+
import { piCompactWouldNoop } from "../mega-pipeline.js";
|
|
11
3
|
import { buildTailResult } from "./context-handler/tailResult.js";
|
|
12
4
|
import { persistEpochAndMaintain } from "./context-handler/afterCompact.js";
|
|
5
|
+
import { appendMirrorAndLedger } from "./context-handler/dbMirrorAppend.js";
|
|
6
|
+
import { evaluateGate } from "./context-handler/gateCheck.js";
|
|
7
|
+
import { invokePipeline } from "./context-handler/pipelineRun.js";
|
|
8
|
+
import { buildLiveTrimView } from "./context-handler/liveTrim.js";
|
|
13
9
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
14
10
|
export function registerContextHandler(pi, runtime, config) {
|
|
15
11
|
// ---- Auto-trigger: live trim (compact and continue) + native durable ----
|
|
@@ -62,107 +58,14 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
62
58
|
return;
|
|
63
59
|
}
|
|
64
60
|
const view = viewForFallback ?? runtime.engineView(messages);
|
|
65
|
-
// S27 DB-mirror
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const db = openStore(runtime.currentStateDir);
|
|
75
|
-
appendMirrorMessages(db, messages, runtime.rt.sessionId, epochIdFor(runtime.rt.sessionId), runtime.currentTurn);
|
|
76
|
-
// P2.2: populate conversation_thread + tool_results tables for
|
|
77
|
-
// prompt-cache analytics and durable separation. The live-array
|
|
78
|
-
// separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
|
|
79
|
-
// tailResult above) is sufficient for the prompt-construction path;
|
|
80
|
-
// these DB writes persist the split for post-hoc analysis, dashboard
|
|
81
|
-
// queries, and future readers. Non-fatal — failure here never breaks
|
|
82
|
-
// the agent loop (PREVENT-PI-004: zero network, local SQLite only).
|
|
83
|
-
{
|
|
84
|
-
const sid = runtime.rt.sessionId;
|
|
85
|
-
const turn = runtime.currentTurn;
|
|
86
|
-
const now = Date.now();
|
|
87
|
-
const threadStmt = db.prepare("INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
|
|
88
|
-
const toolStmt = db.prepare("INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
|
|
89
|
-
for (const m of messages) {
|
|
90
|
-
const role = m.role;
|
|
91
|
-
const content = messageContentText(m);
|
|
92
|
-
if (role === "user" || role === "assistant") {
|
|
93
|
-
threadStmt.run(sid, role, content, turn, now);
|
|
94
|
-
}
|
|
95
|
-
else if (role === "toolResult" || role === "bashExecution") {
|
|
96
|
-
toolStmt.run(sid, role, content, turn, now);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch (e) {
|
|
102
|
-
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// VC1B (S1): canonical messages -> v2 ledger occurrences. Flag-OFF opens
|
|
106
|
-
// no DB (byte-identical predecessor); non-fatal. onFailure surfaces
|
|
107
|
-
// per-append rejections (e.g. EVT_SEQ_REGRESSION on rewind/fork) as
|
|
108
|
-
// structured warnings rather than swallowing them silently.
|
|
109
|
-
try {
|
|
110
|
-
appendMessagesToLedger(runtime.currentStateDir, runtime.rt.sessionId, messages, runtime.logger);
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
runtime.logger.warn("vc1b-ledger-append-fail", { error: String(e) });
|
|
114
|
-
}
|
|
115
|
-
// S52 / v0.16.1: per-model threshold override. The user can tune the
|
|
116
|
-
// fire point + safety margin PER MODEL (different providers' models range
|
|
117
|
-
// 8K-1M+ context, so one global tier % is wrong). Falls back to env/default
|
|
118
|
-
// when no override row exists. Computed once here + reused in the tail cap
|
|
119
|
-
// below; the lookup is a single SQLite PK hit (cheap; cached after the
|
|
120
|
-
// first read in a session).
|
|
121
|
-
const _modelIdForThreshold = runtime.currentModel?.modelId ?? null;
|
|
122
|
-
const _perModelThreshold = resolveModelThreshold(_modelIdForThreshold, {
|
|
123
|
-
safetyMarginFallback: DEFAULT_SAFETY_MARGIN_PCT,
|
|
124
|
-
firePointFallback: config.tierPct != null
|
|
125
|
-
? Math.round(config.tierPct * 100)
|
|
126
|
-
: DEFAULT_FIRE_POINT_PCT,
|
|
127
|
-
stateDir: runtime.currentStateDir,
|
|
128
|
-
});
|
|
129
|
-
// S29 FAST GATE: drive the auto-trigger off the context % (the number the
|
|
130
|
-
// menu bar shows), NOT the token count — the model under-reports tokens,
|
|
131
|
-
// so a token-only gate misses the overshoot that causes max-output-tokens
|
|
132
|
-
// truncation. The fire point is the per-model override when present,
|
|
133
|
-
// otherwise the tier's percent threshold (tierPct) unless overridden by
|
|
134
|
-
// MEGACOMPACT_AUTO_PCT_TRIGGER. `custom` (absolute
|
|
135
|
-
// MEGACOMPACT_THRESHOLD_TOKENS, tierPct null) is an explicit opt-out of
|
|
136
|
-
// percent scaling — it keeps the token gate. When pct is unavailable
|
|
137
|
-
// (window unknown / a model that doesn't report percent) a tiered config
|
|
138
|
-
// falls back to the token gate (S27 boot-fallback guarantee) instead of
|
|
139
|
-
// skipping compaction — a percent-only gate would regress that.
|
|
140
|
-
let gatePassed = false;
|
|
141
|
-
if (config.tierPct != null && pct != null) {
|
|
142
|
-
// Per-model override is a % (10-90); tierPct is a fraction (0.1-1.0).
|
|
143
|
-
// Prefer the override; fall back to autoPctTrigger + tierPct.
|
|
144
|
-
const tierPctFraction = config.autoPctTrigger ?? config.tierPct;
|
|
145
|
-
const perModelFraction = _perModelThreshold.firePointPct / 100;
|
|
146
|
-
const firePct = _modelIdForThreshold != null ? perModelFraction : tierPctFraction;
|
|
147
|
-
gatePassed = pct / 100 >= firePct;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
// custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
|
|
151
|
-
if (currentTokens < runtime.effectiveThreshold) {
|
|
152
|
-
runtime.diagCtxFastGate++;
|
|
153
|
-
return tailResult() ?? undefined;
|
|
154
|
-
}
|
|
155
|
-
const check = autoCompactCheck(currentTokens, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
|
|
156
|
-
if (!check.shouldCompact) {
|
|
157
|
-
runtime.diagCtxNoCompact++;
|
|
158
|
-
return tailResult() ?? undefined;
|
|
159
|
-
}
|
|
160
|
-
gatePassed = true;
|
|
161
|
-
}
|
|
162
|
-
if (!gatePassed) {
|
|
163
|
-
runtime.diagCtxFastGate++;
|
|
164
|
-
return tailResult() ?? undefined;
|
|
165
|
-
}
|
|
61
|
+
// S27 DB-mirror + VC1B ledger append. Runs BEFORE the fast-gate so every
|
|
62
|
+
// message is captured, even if we don't compact this turn. Non-fatal.
|
|
63
|
+
appendMirrorAndLedger(runtime, config, messages);
|
|
64
|
+
// S29 FAST GATE: drive the auto-trigger off the context percent (see
|
|
65
|
+
// gateCheck.ts). Returns a tailed view when the gate does not pass.
|
|
66
|
+
const gate = evaluateGate(runtime, config, { pct, currentTokens, tailResult });
|
|
67
|
+
if (gate.kind === "return")
|
|
68
|
+
return gate.view;
|
|
166
69
|
// D.2: Replay MUST be exempt from debounce — replay is free (no compute,
|
|
167
70
|
// no re-write) and prevents unnecessary KV-cache invalidation. Check
|
|
168
71
|
// replay FIRST, before debounce, so two context events <2s apart both
|
|
@@ -206,36 +109,20 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
206
109
|
return tailResult() ?? undefined;
|
|
207
110
|
}
|
|
208
111
|
runtime.debounceUntil = now + 2000;
|
|
209
|
-
// Adaptive
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
compressionPressure: pressure,
|
|
112
|
+
// Adaptive-compression pipeline invocation (see pipelineRun.ts). Returns a
|
|
113
|
+
// tailed view ("return") when compaction skipped; otherwise "proceed" with
|
|
114
|
+
// the result + pressure consumed by the live-trim stage below.
|
|
115
|
+
const pipeline = invokePipeline(pi, runtime, config, ctx, {
|
|
116
|
+
messages,
|
|
117
|
+
pct,
|
|
118
|
+
currentTokens,
|
|
119
|
+
tailResult,
|
|
218
120
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// (free stability win) — otherwise defer to the next event.
|
|
222
|
-
if (ran.skipped) {
|
|
223
|
-
runtime.diagCtxRunSkipped++;
|
|
224
|
-
if (runtime.trimCache &&
|
|
225
|
-
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
226
|
-
runtime.trimCache.cut <= messages.length) {
|
|
227
|
-
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized by computeLiveTrimCut (src/boundary.ts); replayed verbatim, transcript only grows within an epoch.
|
|
228
|
-
runtime.diagLiveTrimFires++;
|
|
229
|
-
runtime.diagLiveTrimReplays++;
|
|
230
|
-
runtime.snapshot(ctx);
|
|
231
|
-
const skipView = [{ ...runtime.trimCache.summaryAgentMsg }, ...recent];
|
|
232
|
-
return tailResult(skipView) ?? { messages: skipView };
|
|
233
|
-
}
|
|
234
|
-
return tailResult() ?? undefined;
|
|
235
|
-
}
|
|
121
|
+
if (pipeline.kind === "return")
|
|
122
|
+
return pipeline.view;
|
|
236
123
|
// S27 DB-mirror: write checkpoint_epoch + stamp turn epochs + auto-wiki +
|
|
237
124
|
// topic seed + fire-and-forget dedup. Best-effort + non-fatal.
|
|
238
|
-
await persistEpochAndMaintain(runtime, config, ran);
|
|
125
|
+
await persistEpochAndMaintain(runtime, config, pipeline.ran);
|
|
239
126
|
// LEGACY path (rollback): v0.4.28 ctx.compact() + the no-op gate. The
|
|
240
127
|
// manual compact path aborts the in-flight turn — only used behind the flag.
|
|
241
128
|
// Read live from env (in addition to the load-time config) so the flag can be
|
|
@@ -289,172 +176,19 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
289
176
|
return;
|
|
290
177
|
}
|
|
291
178
|
// S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
|
|
292
|
-
// Non-destructive: pi keeps the real transcript; only this
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
: config.anchorUserMessages;
|
|
307
|
-
const cut = computeLiveTrimCut(view, {
|
|
308
|
-
compactedFrom: ran.result.compactedFrom,
|
|
309
|
-
summary: ran.result.summary,
|
|
310
|
-
anchorUserMessages,
|
|
311
|
-
// CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
|
|
312
|
-
// window, relief takes priority over the anchor floor. Without this,
|
|
313
|
-
// computeLiveTrimCut bails to null (can't satisfy the floor) and the
|
|
314
|
-
// model is fed a raw overflow that errors every turn — the
|
|
315
|
-
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
316
|
-
// A thin anchor is recoverable; an overflowed session is not.
|
|
317
|
-
//
|
|
318
|
-
// CRITICAL: pct is null for OpenAI-compatible providers that don't
|
|
319
|
-
// report usage.percent (e.g. neuralwatt). Without the token-pressure
|
|
320
|
-
// fallback the hatch never armed → cut=null → raw overflow → 400
|
|
321
|
-
// "conversation too long even after compaction" (2026-08-03 incident
|
|
322
|
-
// on glm-5.2-short, 200K window). Now also fires on pressure >= 0.9
|
|
323
|
-
// (token-basis) so the hatch arms regardless of whether the provider
|
|
324
|
-
// reports pct.
|
|
325
|
-
criticalOver: (pct ?? 0) >= 90 || pressure >= 0.9,
|
|
326
|
-
});
|
|
327
|
-
if (cut === null) {
|
|
328
|
-
runtime.diagCtxCutNull++;
|
|
329
|
-
runtime.logger.info("live-trim-skip", {
|
|
330
|
-
sessionId: runtime.rt.sessionId,
|
|
331
|
-
compactedFrom: ran.result.compactedFrom,
|
|
332
|
-
viewLen: view.length,
|
|
333
|
-
anchorUserMessages,
|
|
334
|
-
criticalOver: (pct ?? 0) >= 90,
|
|
335
|
-
});
|
|
336
|
-
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
337
|
-
}
|
|
338
|
-
const summaryMsg = liveTrimSummaryMessage({
|
|
339
|
-
compactedFrom: ran.result.compactedFrom,
|
|
340
|
-
summary: ran.result.summary,
|
|
341
|
-
anchorUserMessages: config.anchorUserMessages,
|
|
342
|
-
});
|
|
343
|
-
// Synthesize a user-role AgentMessage carrying the compacted summary.
|
|
344
|
-
const summaryAgentMsg = {
|
|
345
|
-
role: "user",
|
|
346
|
-
content: summaryMsg.text,
|
|
347
|
-
// v0.8.6: stable timestamp across the epoch (NOT Date.now()) so the
|
|
348
|
-
// summary message bytes — and thus the KV-cache prefix — don't drift
|
|
349
|
-
// on every replay within the same compaction epoch.
|
|
350
|
-
timestamp: runtime.rt.lastCompactAt ?? Date.now(),
|
|
351
|
-
};
|
|
352
|
-
const recentRaw = messages.slice(cut); // guardrails-allow PREVENT-PI-002: `cut` is the pre-sanitized `compactedFrom` produced by src/boundary.ts computeDropRange, so the preserved run begins on a toolPair-safe index.
|
|
353
|
-
// FIX 2 (2026-08-03 incident): TOKEN-BUDGET CAP on the live-trim view.
|
|
354
|
-
// Compaction fires at tier% of the window (140K for a 200K window),
|
|
355
|
-
// but a SINGLE turn can inject a huge tool output (file read, bash) that
|
|
356
|
-
// jumps context from 139K → 199K+ before the next gate fires. When that
|
|
357
|
-
// happens [summary + preserved tail] can STILL exceed the model window,
|
|
358
|
-
// and the provider rejects with 400 "conversation too long even after
|
|
359
|
-
// compaction". The anchor floor (PREVENT-PI-001) keeps ≥N user messages
|
|
360
|
-
// but has NO token cap, so a 2-message tail of two 80K bash outputs sails
|
|
361
|
-
// right past the window.
|
|
362
|
-
//
|
|
363
|
-
// Cap: when the model context window is known, reserve room for the
|
|
364
|
-
// summary + the model's max output tokens + a 10% safety margin, then
|
|
365
|
-
// drop oldest preserved messages from the front of `recentRaw` until the
|
|
366
|
-
// tail fits. Never drops below the FINAL message (always keep the latest
|
|
367
|
-
// turn so the agent can respond). This is a last-resort HARD cap — it
|
|
368
|
-
// only fires when the preserved tail alone is oversized, which is rare.
|
|
369
|
-
const ctxWindow = runtime.lastCtxWindow;
|
|
370
|
-
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
371
|
-
const modelThreshold = _perModelThreshold;
|
|
372
|
-
// Reserve room for output tokens. Use the model's reported max output
|
|
373
|
-
// when known; fall back to 10% of the window (scales with any model —
|
|
374
|
-
// 20K for a 200K window, 100K for a 1M window) so we never let the
|
|
375
|
-
// preserved tail eat the model's output budget when maxTokens is unknown.
|
|
376
|
-
const maxOutput = runtime.currentModel?.maxTokens && runtime.currentModel.maxTokens > 0
|
|
377
|
-
? runtime.currentModel.maxTokens
|
|
378
|
-
: Math.ceil(ctxWindow * 0.1);
|
|
379
|
-
let recent = recentRaw;
|
|
380
|
-
if (ctxWindow > 0 && recentRaw.length > 1) {
|
|
381
|
-
const summaryTokens = estimateBlockTokens(summaryMsg.text);
|
|
382
|
-
// Reserve: summary + max output + per-model safety margin (0-20%).
|
|
383
|
-
const safetyMargin = Math.ceil(ctxWindow * (modelThreshold.safetyMarginPct / 100));
|
|
384
|
-
const budget = ctxWindow - maxOutput - safetyMargin - summaryTokens;
|
|
385
|
-
if (budget > 0) {
|
|
386
|
-
// Walk recent from the front, dropping oldest first until the
|
|
387
|
-
// remaining tail fits. Use the AgentMessage→engine-text estimate via
|
|
388
|
-
// messageContentText (already imported) + estimateMessageTokens.
|
|
389
|
-
let tailTokens = 0;
|
|
390
|
-
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
391
|
-
const m = recentRaw[i];
|
|
392
|
-
tailTokens += estimateMessageTokens({
|
|
393
|
-
text: messageContentText(m),
|
|
394
|
-
});
|
|
395
|
-
if (tailTokens > budget) {
|
|
396
|
-
// Keep from i+1 onward; but never fewer than the final message.
|
|
397
|
-
const startIdx = Math.min(i + 1, recentRaw.length - 1);
|
|
398
|
-
if (startIdx > 0) {
|
|
399
|
-
recent = recentRaw.slice(startIdx);
|
|
400
|
-
runtime.logger.warn("live-trim-tail-cap", {
|
|
401
|
-
sessionId: runtime.rt.sessionId,
|
|
402
|
-
dropped: startIdx,
|
|
403
|
-
tailTokens,
|
|
404
|
-
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
405
|
-
budget,
|
|
406
|
-
ctxWindow,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// v0.8.6: cache the trim view so subsequent gated calls in this epoch
|
|
415
|
-
// replay it verbatim (stabilizing the KV-cache prefix) instead of
|
|
416
|
-
// regenerating a fresh summary + sentinel every fire.
|
|
417
|
-
runtime.trimCache = {
|
|
418
|
-
// v0.8.7: key the replay cache on the STABLE epoch signal
|
|
419
|
-
// (rt.lastCheckpointId) instead of ran.result.checkpointId, which is
|
|
420
|
-
// dedup-volatile: on a re-compact that dedups onto a DIFFERENT existing
|
|
421
|
-
// checkpoint, result.checkpointId is the matched id (engine.ts:188) while
|
|
422
|
-
// lastCheckpointId is only updated on a genuinely new checkpoint
|
|
423
|
-
// (compact.ts:100-104). Keying on result.checkpointId would make
|
|
424
|
-
// trimCache.checkpointId != rt.lastCheckpointId forever after that
|
|
425
|
-
// dedup fire, disabling replay for the rest of the epoch (the
|
|
426
|
-
// alternating cache-miss that 0.8.6 meant to fix). Prefer the stable
|
|
427
|
-
// signal; fall back to result.checkpointId then the epoch timestamp
|
|
428
|
-
// only for the no-checkpoint edge case.
|
|
429
|
-
checkpointId: runtime.rt.lastCheckpointId ??
|
|
430
|
-
ran.result.checkpointId ??
|
|
431
|
-
`epoch-${runtime.rt.lastCompactAt ?? Date.now()}`,
|
|
432
|
-
cut,
|
|
433
|
-
summaryAgentMsg,
|
|
434
|
-
ctxPct: pct ?? null,
|
|
435
|
-
ctxTokens: currentTokens,
|
|
436
|
-
};
|
|
437
|
-
runtime.snapshot(ctx);
|
|
438
|
-
// DIAG (team-run relief): confirm the live trim actually fires + how big
|
|
439
|
-
// the window still is. The return is non-durable (per-LLM-call only), so
|
|
440
|
-
// this is the signal that the model is being fed a compacted view while
|
|
441
|
-
// the on-disk transcript + context meter keep growing.
|
|
442
|
-
runtime.diagLiveTrimFires++;
|
|
443
|
-
runtime.logger.info("live-trim", {
|
|
444
|
-
sessionId: runtime.rt.sessionId,
|
|
445
|
-
inputMsgs: messages.length,
|
|
446
|
-
outputMsgs: recent.length + 1,
|
|
447
|
-
compactedFrom: cut,
|
|
448
|
-
ctxPct: pct,
|
|
449
|
-
ctxTokens: usage?.tokens ?? null,
|
|
450
|
-
});
|
|
451
|
-
return (tailResult([summaryAgentMsg, ...recent]) ?? {
|
|
452
|
-
messages: [summaryAgentMsg, ...recent],
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
runtime.diagCtxThrown++;
|
|
457
|
-
return tailResult() ?? undefined; // non-fatal: no trim this call; the next context event retries
|
|
458
|
-
}
|
|
179
|
+
// See liveTrim.ts. Non-destructive: pi keeps the real transcript; only this
|
|
180
|
+
// LLM call sees the trimmed window. Returns undefined on unsafe cut/throw —
|
|
181
|
+
// the next context event retries.
|
|
182
|
+
return buildLiveTrimView(runtime, config, ctx, {
|
|
183
|
+
messages,
|
|
184
|
+
view,
|
|
185
|
+
pct,
|
|
186
|
+
currentTokens,
|
|
187
|
+
usageTokens: usage?.tokens,
|
|
188
|
+
pressure: pipeline.pressure,
|
|
189
|
+
ran: pipeline.ran,
|
|
190
|
+
perModelThreshold: gate.perModelThreshold,
|
|
191
|
+
tailResult,
|
|
192
|
+
});
|
|
459
193
|
});
|
|
460
194
|
}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log.ts — tiny append-only structured logger.
|
|
3
|
+
*
|
|
4
|
+
* Writes one JSON object per line to a log file (default:
|
|
5
|
+
* ~/.pi/agent/extensions/mega-compact.log). Best-effort: logging never throws
|
|
6
|
+
* into the extension. Pi-agnostic and dependency-free so it can be unit-tested.
|
|
7
|
+
*/
|
|
8
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { STATE_DIR_DEFAULT } from "./config.js";
|
|
11
|
+
/** Default log path lives alongside the state dir. */
|
|
12
|
+
export function defaultLogPath() {
|
|
13
|
+
return join(STATE_DIR_DEFAULT, "mega-compact.log");
|
|
14
|
+
}
|
|
15
|
+
export class Logger {
|
|
16
|
+
path;
|
|
17
|
+
enabled;
|
|
18
|
+
/** Monotonic clock injected by the caller so the module stays deterministic. */
|
|
19
|
+
now;
|
|
20
|
+
constructor(opts = {}) {
|
|
21
|
+
this.path = opts.path ?? defaultLogPath();
|
|
22
|
+
this.enabled = opts.enabled ?? true;
|
|
23
|
+
this.now = opts.now ?? (() => Date.now());
|
|
24
|
+
}
|
|
25
|
+
/** Append one structured line. Swallows all I/O errors. */
|
|
26
|
+
log(level, event, fields = {}) {
|
|
27
|
+
if (!this.enabled)
|
|
28
|
+
return;
|
|
29
|
+
const entry = { ts: this.now(), level, event, ...fields };
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
32
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* best-effort: never break the extension on a log failure */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
info(event, fields) {
|
|
39
|
+
this.log("info", event, fields);
|
|
40
|
+
}
|
|
41
|
+
warn(event, fields) {
|
|
42
|
+
this.log("warn", event, fields);
|
|
43
|
+
}
|
|
44
|
+
error(event, fields) {
|
|
45
|
+
this.log("error", event, fields);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -63,6 +63,25 @@ export const VC0C_ENABLED = () => sprintFlag("MEGACOMPACT_VC0C");
|
|
|
63
63
|
* emit seam, the minhash-v2 backfill seam and the downgrade-export seam.
|
|
64
64
|
*/
|
|
65
65
|
export const VC1C_ENABLED = () => sprintFlag("MEGACOMPACT_VC1C");
|
|
66
|
+
/**
|
|
67
|
+
* VC2A — offline model runtime and asset decision (ModelManifestV1 /
|
|
68
|
+
* EncoderRuntime).
|
|
69
|
+
* Default ON. `MEGACOMPACT_VC2A=0` disables and is byte-identical to the
|
|
70
|
+
* predecessor (mode C: no asset manifest is read/verified, the encoder runtime
|
|
71
|
+
* idles in mode C, zero `vector_cortex_encoder_*` emissions; the trigram/lexical
|
|
72
|
+
* paths are unchanged). The real consumers are the encoder emit seam and the
|
|
73
|
+
* encoder runtime's A/B/C selection.
|
|
74
|
+
*/
|
|
75
|
+
export const VC2A_ENABLED = () => sprintFlag("MEGACOMPACT_VC2A");
|
|
76
|
+
/**
|
|
77
|
+
* VC2B — multi-head encoder (VectorSetV1 / HeadCalibrationDraft).
|
|
78
|
+
* Default ON. `MEGACOMPACT_VC2B=0` disables and is byte-identical to the
|
|
79
|
+
* predecessor (the encoder emits no per-head vectors and no fallback-selected
|
|
80
|
+
* event; the trigram/lexical paths themselves are unchanged and are the
|
|
81
|
+
* predecessor's mode-B/C producers). The real consumers are the encoder-heads
|
|
82
|
+
* emit seam and the multi-head encoder producers (heads/trigram/lexical).
|
|
83
|
+
*/
|
|
84
|
+
export const VC2B_ENABLED = () => sprintFlag("MEGACOMPACT_VC2B");
|
|
66
85
|
// ---------------------------------------------------------------------------
|
|
67
86
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
|
|
68
87
|
// Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
|
package/dist/src/config.js
CHANGED
|
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
114
114
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
115
115
|
// so root consumers share one source of truth.
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
-
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
117
|
+
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/asset.ts — VC2A asset verification (task 2).
|
|
3
|
+
*
|
|
4
|
+
* Verifies a ModelManifestV1 before any allocation: SHA-256 the ONNX and
|
|
5
|
+
* tokenizer against the manifest digests, require opset 17, batch exactly 1 and
|
|
6
|
+
* maximum 512 tokens, and confirm the current platform is in the supported
|
|
7
|
+
* matrix. On ANY of these the caller demotes to mode B (asset-free trigram) —
|
|
8
|
+
* never a remote fetch (PREVENT-PI-004). A truncated/unreadable asset during
|
|
9
|
+
* the digest read demotes with ENC_ASSET_UNREADABLE.
|
|
10
|
+
*
|
|
11
|
+
* Pi-agnostic. Filesystem reads only, zero network (PREVENT-PI-004).
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { ENC_FAIL, ENCODER_BATCH, ENCODER_MAX_TOKENS, ENCODER_OPSET, ENCODER_SUPPORTED_PLATFORMS, } from "./types.js";
|
|
17
|
+
/** True when `p` is a single basename: non-empty, no path separators, no "..",
|
|
18
|
+
* no leading dot-segment traversal. Keeps manifest-controlled asset paths
|
|
19
|
+
* confined to the asset directory (no path-traversal via join()). */
|
|
20
|
+
function isBasename(p) {
|
|
21
|
+
if (!p || p.length === 0 || p.includes("/") || p.includes("\\"))
|
|
22
|
+
return false;
|
|
23
|
+
if (p === "." || p === "..")
|
|
24
|
+
return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/** Digest the on-disk bytes of one asset file; "" on unreadable (truncated). */
|
|
28
|
+
function digestFile(path) {
|
|
29
|
+
try {
|
|
30
|
+
const buf = readFileSync(path);
|
|
31
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Truncated / unreadable during the digest read -> ENC_ASSET_UNREADABLE.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Detect the current platform (MODEL_ASSET supported matrix). Unrecognized
|
|
40
|
+
* hosts return null so verification demotes to mode B (unsupported platform).
|
|
41
|
+
*/
|
|
42
|
+
export function detectPlatform(host = process.platform, arch = process.arch) {
|
|
43
|
+
if (!host || !arch)
|
|
44
|
+
return null;
|
|
45
|
+
// Normalize "win32"/"win32"/"linux"/"darwin" + "x64"/"arm64".
|
|
46
|
+
const plat = host === "win32" ? "win32" : host === "darwin" ? "darwin" : host === "linux" ? "linux" : "";
|
|
47
|
+
const a = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : "";
|
|
48
|
+
if (!plat || !a)
|
|
49
|
+
return null;
|
|
50
|
+
const candidate = `${plat}-${a}`;
|
|
51
|
+
return ENCODER_SUPPORTED_PLATFORMS.includes(candidate)
|
|
52
|
+
? candidate
|
|
53
|
+
: null;
|
|
54
|
+
}
|
|
55
|
+
function isManifest(m) {
|
|
56
|
+
const o = m;
|
|
57
|
+
return (!!o &&
|
|
58
|
+
typeof o === "object" &&
|
|
59
|
+
o.schema === "model-manifest-v1" &&
|
|
60
|
+
typeof o.opset === "number" &&
|
|
61
|
+
typeof o.batch === "number" &&
|
|
62
|
+
typeof o.maxTokens === "number" &&
|
|
63
|
+
typeof o.platform === "string" &&
|
|
64
|
+
!!o.onnx &&
|
|
65
|
+
!!o.tokenizer &&
|
|
66
|
+
typeof o.onnx.path === "string" &&
|
|
67
|
+
typeof o.onnx.sha256 === "string" &&
|
|
68
|
+
typeof o.tokenizer.path === "string" &&
|
|
69
|
+
typeof o.tokenizer.sha256 === "string");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Verify the asset manifest + digest + constraints BEFORE allocation.
|
|
73
|
+
*
|
|
74
|
+
* - manifest parse/shape failure -> ENC_MANIFEST_INVALID -> mode B
|
|
75
|
+
* - unsupported platform -> ENC_PLATFORM_UNSUPPORTED -> mode B
|
|
76
|
+
* - opset != 17 -> ENC_OPSET_INVALID -> mode B
|
|
77
|
+
* - batch != 1 -> ENC_BATCH_INVALID -> mode B
|
|
78
|
+
* - maxTokens > 512 -> ENC_TOKENS_EXCEEDED -> mode B
|
|
79
|
+
* - on-disk digest != manifest digest -> ENC_DIGEST_MISMATCH (one-byte mutation)
|
|
80
|
+
* - unreadable/truncated file -> ENC_ASSET_UNREADABLE
|
|
81
|
+
*
|
|
82
|
+
* Returns ok only when EVERY constraint passes and both files hash to the
|
|
83
|
+
* declared digests (the "only batch1/max512 verified assets reach inference"
|
|
84
|
+
* invariant). The ok result surfaces the verified manifest's `maxTokens`
|
|
85
|
+
* (<= 512) so the runtime can enforce the per-manifest token capacity at
|
|
86
|
+
* inference (Q03), not just the global 512 ceiling.
|
|
87
|
+
*/
|
|
88
|
+
export function verifyEncoderAsset(assetDir, manifest, platform = detectPlatform()) {
|
|
89
|
+
if (typeof manifest !== "object" || manifest === null || !isManifest(manifest)) {
|
|
90
|
+
return { ok: false, code: ENC_FAIL.MANIFEST_INVALID };
|
|
91
|
+
}
|
|
92
|
+
if (!platform)
|
|
93
|
+
return { ok: false, code: ENC_FAIL.PLATFORM_UNSUPPORTED };
|
|
94
|
+
// The manifest's declared platform must match the runtime host (per-platform
|
|
95
|
+
// asset pinning): a bundle cross-shipped to the wrong arch is not qualified.
|
|
96
|
+
if (manifest.platform !== platform)
|
|
97
|
+
return { ok: false, code: ENC_FAIL.PLATFORM_UNSUPPORTED };
|
|
98
|
+
if (manifest.opset !== ENCODER_OPSET)
|
|
99
|
+
return { ok: false, code: ENC_FAIL.OPSET_INVALID };
|
|
100
|
+
if (manifest.batch !== ENCODER_BATCH)
|
|
101
|
+
return { ok: false, code: ENC_FAIL.BATCH_INVALID };
|
|
102
|
+
if (manifest.maxTokens > ENCODER_MAX_TOKENS)
|
|
103
|
+
return { ok: false, code: ENC_FAIL.TOKENS_EXCEEDED };
|
|
104
|
+
// Constrain asset paths to basenames (no separators / no '..') so a forged
|
|
105
|
+
// manifest cannot read digests from arbitrary paths off the asset dir.
|
|
106
|
+
if (!isBasename(manifest.onnx.path) || !isBasename(manifest.tokenizer.path)) {
|
|
107
|
+
return { ok: false, code: ENC_FAIL.MANIFEST_INVALID };
|
|
108
|
+
}
|
|
109
|
+
const onnxPath = join(assetDir, manifest.onnx.path);
|
|
110
|
+
const onnxDigest = digestFile(onnxPath);
|
|
111
|
+
if (onnxDigest === null)
|
|
112
|
+
return { ok: false, code: ENC_FAIL.ASSET_UNREADABLE };
|
|
113
|
+
if (onnxDigest !== manifest.onnx.sha256)
|
|
114
|
+
return { ok: false, code: ENC_FAIL.DIGEST_MISMATCH };
|
|
115
|
+
const tokPath = join(assetDir, manifest.tokenizer.path);
|
|
116
|
+
const tokDigest = digestFile(tokPath);
|
|
117
|
+
if (tokDigest === null)
|
|
118
|
+
return { ok: false, code: ENC_FAIL.ASSET_UNREADABLE };
|
|
119
|
+
if (tokDigest !== manifest.tokenizer.sha256)
|
|
120
|
+
return { ok: false, code: ENC_FAIL.DIGEST_MISMATCH };
|
|
121
|
+
let embeddedBytes = 0;
|
|
122
|
+
try {
|
|
123
|
+
embeddedBytes = statSync(onnxPath).size + statSync(tokPath).size;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { ok: false, code: ENC_FAIL.ASSET_UNREADABLE };
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, embeddedBytes, maxTokens: manifest.maxTokens, onnxDigest, tokenizerDigest: tokDigest };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read + shape-check a committed ModelManifestV1 from an asset directory.
|
|
132
|
+
* Returns the parsed manifest or null when the file is absent/malformed.
|
|
133
|
+
*/
|
|
134
|
+
export function readEncoderManifest(assetDir) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = JSON.parse(readFileSync(join(assetDir, "manifest.json"), "utf8"));
|
|
137
|
+
return isManifest(raw) ? raw : null;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|