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
|
@@ -270,6 +270,8 @@ export const SETTINGS: ReadonlyArray<{
|
|
|
270
270
|
str("MEGACOMPACT_RAPTOR_MODEL", "RAPTOR Summary Model", "Ollama model for cluster summarization (empty = extractive)", ""),
|
|
271
271
|
str("MEGACOMPACT_RAPTOR_URL", "RAPTOR Ollama URL", "Ollama endpoint for RAPTOR summarization", "http://127.0.0.1:11434"),
|
|
272
272
|
num("MEGACOMPACT_EMBED_CACHE", "Embed Cache Size", "Embedding cache entries (0 = disabled)", 256, 0, 10000),
|
|
273
|
+
num("MEGACOMPACT_EMBEDDING_BATCH_TOKENS", "Embedding Batch Tokens", "Oversized-prompt chunking limit (tokens) for the BYO localhost embedder; text above this is chunked + mean-pooled", 2048, 64, 8192),
|
|
274
|
+
num("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", "Embedding Chars per Token", "Estimated characters per token used for embedder chunking size", 4, 1, 32),
|
|
273
275
|
],
|
|
274
276
|
},
|
|
275
277
|
{
|
|
@@ -311,6 +313,18 @@ export const SETTINGS: ReadonlyArray<{
|
|
|
311
313
|
"FixtureManifestV2 canonical manifest validator + DowngradeReport deterministic downgrade export + MinHashV2 exact big-integer signatures and the M4 copy/validate/switch minhash-v2 migration (seed table frozen, cross-language byte-exact). OFF = mode C, v1 sync dedup scan unchanged, byte-identical.",
|
|
312
314
|
true,
|
|
313
315
|
),
|
|
316
|
+
boolDirect(
|
|
317
|
+
"MEGACOMPACT_VC2A",
|
|
318
|
+
"VC2A Offline Model Runtime",
|
|
319
|
+
"ModelManifestV1 digest-before-load ONNX runtime (opset17/batch1/max512) + asset-free trigram demotion. Asset path assets/vector-cortex/encoder-v1 is immutable/digest-pinned. OFF = mode C, byte-identical to predecessor.",
|
|
320
|
+
true,
|
|
321
|
+
),
|
|
322
|
+
boolDirect(
|
|
323
|
+
"MEGACOMPACT_VC2B",
|
|
324
|
+
"VC2B Multi-Head Encoder",
|
|
325
|
+
"VectorSetV1 five L2-normalized heads (384/128/128/64/32) with head-calibration draft + asset-free trigram B (512d) and lexical C fallbacks, plus the per-head emit seam. OFF = mode C, no per-head vectors emitted, byte-identical predecessor.",
|
|
326
|
+
true,
|
|
327
|
+
),
|
|
314
328
|
],
|
|
315
329
|
},
|
|
316
330
|
{
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/dbMirrorAppend.ts — DB-mirror append + VC1B ledger append.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from context-handler.ts (delegate-shell split). Appends incoming
|
|
5
|
+
* messages to raw_transcript (S27) + conversation_thread/tool_results (P2.2),
|
|
6
|
+
* then appends canonical messages to the v2 vector-cortex ledger (VC1B S1).
|
|
7
|
+
* All best-effort + non-fatal — a failure never breaks the agent loop
|
|
8
|
+
* (PREVENT-PI-004: zero network, local SQLite only).
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
11
|
+
import { openStore } from "../../../src/store/sqlite.js";
|
|
12
|
+
import { appendMirrorMessages } from "../mirror-append.js";
|
|
13
|
+
import { appendMessagesToLedger } from "../../mega-runtime/vector-cortex-ledger.js";
|
|
14
|
+
import { epochIdFor } from "../../../src/mirror/epoch.js";
|
|
15
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
16
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
17
|
+
import { messageContentText } from "./messageText.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Append incoming messages to the DB mirror (raw_transcript + thread/tool
|
|
21
|
+
* tables) and the v2 ledger. Gated on config.dbMirror for the mirror; the VC1B
|
|
22
|
+
* ledger append is flag-gated inside appendMessagesToLedger (flag-OFF opens no
|
|
23
|
+
* DB, byte-identical to the predecessor). Non-fatal end-to-end.
|
|
24
|
+
*/
|
|
25
|
+
export function appendMirrorAndLedger(
|
|
26
|
+
runtime: MegaRuntime,
|
|
27
|
+
config: MegaConfig,
|
|
28
|
+
messages: AgentMessage[],
|
|
29
|
+
): void {
|
|
30
|
+
// S27 DB-mirror: append incoming messages to raw_transcript.
|
|
31
|
+
// Runs BEFORE fast-gate so every message is captured, even if we
|
|
32
|
+
// don't compact this turn. Append is idempotent (content_hash PK).
|
|
33
|
+
// F3: high-water mark (mirror-append.ts) skips already-processed
|
|
34
|
+
// messages on subsequent events. On fork/rewind (shorter list or
|
|
35
|
+
// boundary hash mismatch) the mark is dropped, falling back to a
|
|
36
|
+
// full reprocess.
|
|
37
|
+
if (config.dbMirror) {
|
|
38
|
+
try {
|
|
39
|
+
const db = openStore(runtime.currentStateDir);
|
|
40
|
+
appendMirrorMessages(
|
|
41
|
+
db,
|
|
42
|
+
messages,
|
|
43
|
+
runtime.rt.sessionId,
|
|
44
|
+
epochIdFor(runtime.rt.sessionId),
|
|
45
|
+
runtime.currentTurn,
|
|
46
|
+
);
|
|
47
|
+
// P2.2: populate conversation_thread + tool_results tables for
|
|
48
|
+
// prompt-cache analytics and durable separation. The live-array
|
|
49
|
+
// separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
|
|
50
|
+
// tailResult) is sufficient for the prompt-construction path;
|
|
51
|
+
// these DB writes persist the split for post-hoc analysis, dashboard
|
|
52
|
+
// queries, and future readers. Non-fatal — failure here never breaks
|
|
53
|
+
// the agent loop (PREVENT-PI-004: zero network, local SQLite only).
|
|
54
|
+
{
|
|
55
|
+
const sid = runtime.rt.sessionId;
|
|
56
|
+
const turn = runtime.currentTurn;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const threadStmt = db.prepare(
|
|
59
|
+
"INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
60
|
+
);
|
|
61
|
+
const toolStmt = db.prepare(
|
|
62
|
+
"INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
63
|
+
);
|
|
64
|
+
for (const m of messages) {
|
|
65
|
+
const role = m.role;
|
|
66
|
+
const content = messageContentText(m);
|
|
67
|
+
if (role === "user" || role === "assistant") {
|
|
68
|
+
threadStmt.run(sid, role, content, turn, now);
|
|
69
|
+
} else if (role === "toolResult" || role === "bashExecution") {
|
|
70
|
+
toolStmt.run(sid, role, content, turn, now);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// VC1B (S1): canonical messages -> v2 ledger occurrences. Flag-OFF opens
|
|
80
|
+
// no DB (byte-identical predecessor); non-fatal. onFailure surfaces
|
|
81
|
+
// per-append rejections (e.g. EVT_SEQ_REGRESSION on rewind/fork) as
|
|
82
|
+
// structured warnings rather than swallowing them silently.
|
|
83
|
+
try {
|
|
84
|
+
appendMessagesToLedger(
|
|
85
|
+
runtime.currentStateDir,
|
|
86
|
+
runtime.rt.sessionId,
|
|
87
|
+
messages,
|
|
88
|
+
runtime.logger,
|
|
89
|
+
);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
runtime.logger.warn("vc1b-ledger-append-fail", { error: String(e) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/gateCheck.ts — S29 fast-gate threshold evaluation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from context-handler.ts (delegate-shell split). Drives the
|
|
5
|
+
* auto-trigger off the context % (the number the menu bar shows), NOT the
|
|
6
|
+
* token count — the model under-reports tokens, so a token-only gate misses
|
|
7
|
+
* the overshoot that causes max-output-tokens truncation. Returns a
|
|
8
|
+
* discriminated union: either "return" (a tailed view to hand back to pi) or
|
|
9
|
+
* "proceed" with the resolved per-model threshold for the live-trim tail cap.
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
12
|
+
import {
|
|
13
|
+
resolveModelThreshold,
|
|
14
|
+
DEFAULT_SAFETY_MARGIN_PCT,
|
|
15
|
+
DEFAULT_FIRE_POINT_PCT,
|
|
16
|
+
} from "../../../src/store/sqlite.js";
|
|
17
|
+
import { autoCompactCheck } from "../../../src/compact.js";
|
|
18
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
19
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
20
|
+
|
|
21
|
+
/** Tail-injection closure shape produced by buildTailResult (tailResult.ts). */
|
|
22
|
+
export type TailResultFn = (
|
|
23
|
+
msgs?: readonly AgentMessage[],
|
|
24
|
+
) => { messages: AgentMessage[] } | undefined;
|
|
25
|
+
|
|
26
|
+
/** Outcome of the fast-gate evaluation. */
|
|
27
|
+
export type GateOutcome =
|
|
28
|
+
| { kind: "return"; view: { messages: AgentMessage[] } | undefined }
|
|
29
|
+
| {
|
|
30
|
+
kind: "proceed";
|
|
31
|
+
perModelThreshold: { safetyMarginPct: number; firePointPct: number };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate whether the current context warrants compaction. Returns a tailed
|
|
36
|
+
* view ("return") when the gate does not pass, or "proceed" with the resolved
|
|
37
|
+
* per-model threshold (reused by the live-trim token-budget tail cap).
|
|
38
|
+
*/
|
|
39
|
+
export function evaluateGate(
|
|
40
|
+
runtime: MegaRuntime,
|
|
41
|
+
config: MegaConfig,
|
|
42
|
+
opts: {
|
|
43
|
+
pct: number | null | undefined;
|
|
44
|
+
currentTokens: number;
|
|
45
|
+
tailResult: TailResultFn;
|
|
46
|
+
},
|
|
47
|
+
): GateOutcome {
|
|
48
|
+
const pct = opts.pct;
|
|
49
|
+
const currentTokens = opts.currentTokens;
|
|
50
|
+
const tailResult = opts.tailResult;
|
|
51
|
+
|
|
52
|
+
// S52 / v0.16.1: per-model threshold override. The user can tune the
|
|
53
|
+
// fire point + safety margin PER MODEL (different providers' models range
|
|
54
|
+
// 8K-1M+ context, so one global tier % is wrong). Falls back to env/default
|
|
55
|
+
// when no override row exists. Computed once here + reused in the tail cap
|
|
56
|
+
// below; the lookup is a single SQLite PK hit (cheap; cached after the
|
|
57
|
+
// first read in a session).
|
|
58
|
+
const modelIdForThreshold = runtime.currentModel?.modelId ?? null;
|
|
59
|
+
const perModelThreshold = resolveModelThreshold(modelIdForThreshold, {
|
|
60
|
+
safetyMarginFallback: DEFAULT_SAFETY_MARGIN_PCT,
|
|
61
|
+
firePointFallback:
|
|
62
|
+
config.tierPct != null
|
|
63
|
+
? Math.round(config.tierPct * 100)
|
|
64
|
+
: DEFAULT_FIRE_POINT_PCT,
|
|
65
|
+
stateDir: runtime.currentStateDir,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// S29 FAST GATE: `custom` (absolute MEGACOMPACT_THRESHOLD_TOKENS,
|
|
69
|
+
// tierPct null) is an explicit opt-out of percent scaling — it keeps the
|
|
70
|
+
// token gate. When pct is unavailable (window unknown / a model that
|
|
71
|
+
// doesn't report percent) a tiered config falls back to the token gate
|
|
72
|
+
// (S27 boot-fallback guarantee) instead of skipping compaction — a
|
|
73
|
+
// percent-only gate would regress that.
|
|
74
|
+
let gatePassed = false;
|
|
75
|
+
if (config.tierPct != null && pct != null) {
|
|
76
|
+
// Per-model override is a % (10-90); tierPct is a fraction (0.1-1.0).
|
|
77
|
+
// Prefer the override; fall back to autoPctTrigger + tierPct.
|
|
78
|
+
const tierPctFraction = config.autoPctTrigger ?? config.tierPct;
|
|
79
|
+
const perModelFraction = perModelThreshold.firePointPct / 100;
|
|
80
|
+
const firePct =
|
|
81
|
+
modelIdForThreshold != null ? perModelFraction : tierPctFraction;
|
|
82
|
+
gatePassed = pct / 100 >= firePct;
|
|
83
|
+
} else {
|
|
84
|
+
// custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
|
|
85
|
+
if (currentTokens < runtime.effectiveThreshold) {
|
|
86
|
+
runtime.diagCtxFastGate++;
|
|
87
|
+
return { kind: "return", view: tailResult() ?? undefined };
|
|
88
|
+
}
|
|
89
|
+
const check = autoCompactCheck(currentTokens, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
|
|
90
|
+
if (!check.shouldCompact) {
|
|
91
|
+
runtime.diagCtxNoCompact++;
|
|
92
|
+
return { kind: "return", view: tailResult() ?? undefined };
|
|
93
|
+
}
|
|
94
|
+
gatePassed = true;
|
|
95
|
+
}
|
|
96
|
+
if (!gatePassed) {
|
|
97
|
+
runtime.diagCtxFastGate++;
|
|
98
|
+
return { kind: "return", view: tailResult() ?? undefined };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "proceed", perModelThreshold };
|
|
101
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/liveTrim.ts — S16 live-trim view reconstruction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from context-handler.ts (delegate-shell split). Collapses the
|
|
5
|
+
* compacted region to a summary + recent anchor for THIS LLM call only (pi
|
|
6
|
+
* keeps the real transcript; the trim is non-destructive). Computes the cut on
|
|
7
|
+
* the engine view (pure, tested) then slices the ORIGINAL pi AgentMessage[]
|
|
8
|
+
* from that index (lossless alignment) and prepends a user-role summary. A
|
|
9
|
+
* build failure or unsafe cut returns nothing (no trim this call — the next
|
|
10
|
+
* context event retries). Includes the FIX 2 token-budget cap so a single
|
|
11
|
+
* oversized tool output can't sail past the model window.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
15
|
+
import type { EngineMessage } from "../../../src/types.js";
|
|
16
|
+
import {
|
|
17
|
+
estimateBlockTokens,
|
|
18
|
+
estimateMessageTokens,
|
|
19
|
+
} from "../../../src/tokens.js";
|
|
20
|
+
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../../mega-trim.js";
|
|
21
|
+
import { messageContentText } from "./messageText.js";
|
|
22
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
23
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
24
|
+
import type { TailResultFn } from "./gateCheck.js";
|
|
25
|
+
|
|
26
|
+
/** Shape of the compact result consumed by the live-trim cut computation. */
|
|
27
|
+
interface CompactResult {
|
|
28
|
+
checkpointId?: string;
|
|
29
|
+
compactedFrom: number;
|
|
30
|
+
summary: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconstruct the live-trim window (summary + recent anchor) for this LLM
|
|
35
|
+
* call. Returns the tailed view, or undefined when no trim is safe this call.
|
|
36
|
+
*/
|
|
37
|
+
export function buildLiveTrimView(
|
|
38
|
+
runtime: MegaRuntime,
|
|
39
|
+
config: MegaConfig,
|
|
40
|
+
ctx: ExtensionContext,
|
|
41
|
+
opts: {
|
|
42
|
+
messages: readonly AgentMessage[];
|
|
43
|
+
view: EngineMessage[];
|
|
44
|
+
pct: number | null | undefined;
|
|
45
|
+
currentTokens: number;
|
|
46
|
+
usageTokens: number | null | undefined;
|
|
47
|
+
pressure: number;
|
|
48
|
+
ran: { result: CompactResult };
|
|
49
|
+
perModelThreshold: { safetyMarginPct: number; firePointPct: number };
|
|
50
|
+
tailResult: TailResultFn;
|
|
51
|
+
},
|
|
52
|
+
): { messages: AgentMessage[] } | undefined {
|
|
53
|
+
const {
|
|
54
|
+
messages,
|
|
55
|
+
view,
|
|
56
|
+
pct,
|
|
57
|
+
currentTokens,
|
|
58
|
+
usageTokens,
|
|
59
|
+
pressure,
|
|
60
|
+
ran,
|
|
61
|
+
perModelThreshold,
|
|
62
|
+
tailResult,
|
|
63
|
+
} = opts;
|
|
64
|
+
|
|
65
|
+
// S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
|
|
66
|
+
// Non-destructive: pi keeps the real transcript; only this LLM call sees the
|
|
67
|
+
// trimmed window. We compute the cut on the engine view (pure, tested) then
|
|
68
|
+
// slice the ORIGINAL pi AgentMessage[] from that index (lossless alignment,
|
|
69
|
+
// mirroring dropCompactedRange) and prepend a user-role summary message.
|
|
70
|
+
// A build failure or unsafe cut returns nothing (no trim this call — the
|
|
71
|
+
// next context event retries). The anchor floor is read live from env (the
|
|
72
|
+
// config value is the cached default) so it can be tuned per-test / per-run
|
|
73
|
+
// without reloading the module.
|
|
74
|
+
try {
|
|
75
|
+
const anchorEnv = process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
76
|
+
const anchorUserMessages =
|
|
77
|
+
anchorEnv != null &&
|
|
78
|
+
anchorEnv !== "" &&
|
|
79
|
+
Number.isFinite(Number(anchorEnv))
|
|
80
|
+
? Number(anchorEnv)
|
|
81
|
+
: config.anchorUserMessages;
|
|
82
|
+
const cut = computeLiveTrimCut(view, {
|
|
83
|
+
compactedFrom: ran.result.compactedFrom,
|
|
84
|
+
summary: ran.result.summary,
|
|
85
|
+
anchorUserMessages,
|
|
86
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
|
|
87
|
+
// window, relief takes priority over the anchor floor. Without this,
|
|
88
|
+
// computeLiveTrimCut bails to null (can't satisfy the floor) and the
|
|
89
|
+
// model is fed a raw overflow that errors every turn — the
|
|
90
|
+
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
91
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
92
|
+
//
|
|
93
|
+
// CRITICAL: pct is null for OpenAI-compatible providers that don't
|
|
94
|
+
// report usage.percent (e.g. neuralwatt). Without the token-pressure
|
|
95
|
+
// fallback the hatch never armed → cut=null → raw overflow → 400
|
|
96
|
+
// "conversation too long even after compaction" (2026-08-03 incident
|
|
97
|
+
// on glm-5.2-short, 200K window). Now also fires on pressure >= 0.9
|
|
98
|
+
// (token-basis) so the hatch arms regardless of whether the provider
|
|
99
|
+
// reports pct.
|
|
100
|
+
criticalOver: (pct ?? 0) >= 90 || pressure >= 0.9,
|
|
101
|
+
});
|
|
102
|
+
if (cut === null) {
|
|
103
|
+
runtime.diagCtxCutNull++;
|
|
104
|
+
runtime.logger.info("live-trim-skip", {
|
|
105
|
+
sessionId: runtime.rt.sessionId,
|
|
106
|
+
compactedFrom: ran.result.compactedFrom,
|
|
107
|
+
viewLen: view.length,
|
|
108
|
+
anchorUserMessages,
|
|
109
|
+
criticalOver: (pct ?? 0) >= 90,
|
|
110
|
+
});
|
|
111
|
+
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
112
|
+
}
|
|
113
|
+
const summaryMsg = liveTrimSummaryMessage({
|
|
114
|
+
compactedFrom: ran.result.compactedFrom,
|
|
115
|
+
summary: ran.result.summary,
|
|
116
|
+
anchorUserMessages: config.anchorUserMessages,
|
|
117
|
+
});
|
|
118
|
+
// Synthesize a user-role AgentMessage carrying the compacted summary.
|
|
119
|
+
const summaryAgentMsg = {
|
|
120
|
+
role: "user" as const,
|
|
121
|
+
content: summaryMsg.text,
|
|
122
|
+
// v0.8.6: stable timestamp across the epoch (NOT Date.now()) so the
|
|
123
|
+
// summary message bytes — and thus the KV-cache prefix — don't drift
|
|
124
|
+
// on every replay within the same compaction epoch.
|
|
125
|
+
timestamp: runtime.rt.lastCompactAt ?? Date.now(),
|
|
126
|
+
} as unknown as AgentMessage;
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
// FIX 2 (2026-08-03 incident): TOKEN-BUDGET CAP on the live-trim view.
|
|
130
|
+
// Compaction fires at tier% of the window (140K for a 200K window),
|
|
131
|
+
// but a SINGLE turn can inject a huge tool output (file read, bash) that
|
|
132
|
+
// jumps context from 139K → 199K+ before the next gate fires. When that
|
|
133
|
+
// happens [summary + preserved tail] can STILL exceed the model window,
|
|
134
|
+
// and the provider rejects with 400 "conversation too long even after
|
|
135
|
+
// compaction". The anchor floor (PREVENT-PI-001) keeps ≥N user messages
|
|
136
|
+
// but has NO token cap, so a 2-message tail of two 80K bash outputs sails
|
|
137
|
+
// right past the window.
|
|
138
|
+
//
|
|
139
|
+
// Cap: when the model context window is known, reserve room for the
|
|
140
|
+
// summary + the model's max output tokens + a 10% safety margin, then
|
|
141
|
+
// drop oldest preserved messages from the front of `recentRaw` until the
|
|
142
|
+
// tail fits. Never drops below the FINAL message (always keep the latest
|
|
143
|
+
// turn so the agent can respond). This is a last-resort HARD cap — it
|
|
144
|
+
// only fires when the preserved tail alone is oversized, which is rare.
|
|
145
|
+
const ctxWindow = runtime.lastCtxWindow;
|
|
146
|
+
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
147
|
+
const modelThreshold = perModelThreshold;
|
|
148
|
+
// Reserve room for output tokens. Use the model's reported max output
|
|
149
|
+
// when known; fall back to 10% of the window (scales with any model —
|
|
150
|
+
// 20K for a 200K window, 100K for a 1M window) so we never let the
|
|
151
|
+
// preserved tail eat the model's output budget when maxTokens is unknown.
|
|
152
|
+
const maxOutput =
|
|
153
|
+
runtime.currentModel?.maxTokens && runtime.currentModel.maxTokens > 0
|
|
154
|
+
? runtime.currentModel.maxTokens
|
|
155
|
+
: Math.ceil(ctxWindow * 0.1);
|
|
156
|
+
let recent = recentRaw;
|
|
157
|
+
if (ctxWindow > 0 && recentRaw.length > 1) {
|
|
158
|
+
const summaryTokens = estimateBlockTokens(summaryMsg.text);
|
|
159
|
+
// Reserve: summary + max output + per-model safety margin (0-20%).
|
|
160
|
+
const safetyMargin = Math.ceil(
|
|
161
|
+
ctxWindow * (modelThreshold.safetyMarginPct / 100),
|
|
162
|
+
);
|
|
163
|
+
const budget = ctxWindow - maxOutput - safetyMargin - summaryTokens;
|
|
164
|
+
if (budget > 0) {
|
|
165
|
+
// Walk recent from the front, dropping oldest first until the
|
|
166
|
+
// remaining tail fits. Use the AgentMessage→engine-text estimate via
|
|
167
|
+
// messageContentText (already imported) + estimateMessageTokens.
|
|
168
|
+
let tailTokens = 0;
|
|
169
|
+
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
170
|
+
const m = recentRaw[i];
|
|
171
|
+
tailTokens += estimateMessageTokens({
|
|
172
|
+
text: messageContentText(m),
|
|
173
|
+
});
|
|
174
|
+
if (tailTokens > budget) {
|
|
175
|
+
// Keep from i+1 onward; but never fewer than the final message.
|
|
176
|
+
const startIdx = Math.min(i + 1, recentRaw.length - 1);
|
|
177
|
+
if (startIdx > 0) {
|
|
178
|
+
recent = recentRaw.slice(startIdx);
|
|
179
|
+
runtime.logger.warn("live-trim-tail-cap", {
|
|
180
|
+
sessionId: runtime.rt.sessionId,
|
|
181
|
+
dropped: startIdx,
|
|
182
|
+
tailTokens,
|
|
183
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
184
|
+
budget,
|
|
185
|
+
ctxWindow,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// v0.8.6: cache the trim view so subsequent gated calls in this epoch
|
|
195
|
+
// replay it verbatim (stabilizing the KV-cache prefix) instead of
|
|
196
|
+
// regenerating a fresh summary + sentinel every fire.
|
|
197
|
+
runtime.trimCache = {
|
|
198
|
+
// v0.8.7: key the replay cache on the STABLE epoch signal
|
|
199
|
+
// (rt.lastCheckpointId) instead of ran.result.checkpointId, which is
|
|
200
|
+
// dedup-volatile: on a re-compact that dedups onto a DIFFERENT existing
|
|
201
|
+
// checkpoint, result.checkpointId is the matched id (engine.ts:188) while
|
|
202
|
+
// lastCheckpointId is only updated on a genuinely new checkpoint
|
|
203
|
+
// (compact.ts:100-104). Keying on result.checkpointId would make
|
|
204
|
+
// trimCache.checkpointId != rt.lastCheckpointId forever after that
|
|
205
|
+
// dedup fire, disabling replay for the rest of the epoch (the
|
|
206
|
+
// alternating cache-miss that 0.8.6 meant to fix). Prefer the stable
|
|
207
|
+
// signal; fall back to result.checkpointId then the epoch timestamp
|
|
208
|
+
// only for the no-checkpoint edge case.
|
|
209
|
+
checkpointId:
|
|
210
|
+
runtime.rt.lastCheckpointId ??
|
|
211
|
+
ran.result.checkpointId ??
|
|
212
|
+
`epoch-${runtime.rt.lastCompactAt ?? Date.now()}`,
|
|
213
|
+
cut,
|
|
214
|
+
summaryAgentMsg,
|
|
215
|
+
ctxPct: pct ?? null,
|
|
216
|
+
ctxTokens: currentTokens,
|
|
217
|
+
};
|
|
218
|
+
runtime.snapshot(ctx);
|
|
219
|
+
// DIAG (team-run relief): confirm the live trim actually fires + how big
|
|
220
|
+
// the window still is. The return is non-durable (per-LLM-call only), so
|
|
221
|
+
// this is the signal that the model is being fed a compacted view while
|
|
222
|
+
// the on-disk transcript + context meter keep growing.
|
|
223
|
+
runtime.diagLiveTrimFires++;
|
|
224
|
+
runtime.logger.info("live-trim", {
|
|
225
|
+
sessionId: runtime.rt.sessionId,
|
|
226
|
+
inputMsgs: messages.length,
|
|
227
|
+
outputMsgs: recent.length + 1,
|
|
228
|
+
compactedFrom: cut,
|
|
229
|
+
ctxPct: pct,
|
|
230
|
+
ctxTokens: usageTokens,
|
|
231
|
+
});
|
|
232
|
+
return (
|
|
233
|
+
tailResult([summaryAgentMsg, ...recent]) ?? {
|
|
234
|
+
messages: [summaryAgentMsg, ...recent],
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
runtime.diagCtxThrown++;
|
|
239
|
+
return tailResult() ?? undefined; // non-fatal: no trim this call; the next context event retries
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/pipelineRun.ts — adaptive-compression pipeline invocation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from context-handler.ts (delegate-shell split). Scales compression
|
|
5
|
+
* strength + keepFrom depth with how close the context is to the model limit
|
|
6
|
+
* (Fix E), invokes runCompact, and routes "skipped" outcomes back to a replay
|
|
7
|
+
* of the cached trim view (D.3 free-stability win) instead of returning empty.
|
|
8
|
+
* Returns a discriminated union: "return" (a tailed view to hand back to pi)
|
|
9
|
+
* or "proceed" with the compact result + pressure for the live-trim stage.
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
ExtensionAPI,
|
|
13
|
+
ExtensionContext,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
16
|
+
import type { RunCompactResult } from "../../mega-pipeline.js";
|
|
17
|
+
import { runCompact } from "../../mega-pipeline.js";
|
|
18
|
+
import { pressureFromPct, pressureRatio } from "../../mega-config.js";
|
|
19
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
20
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
21
|
+
import type { TailResultFn } from "./gateCheck.js";
|
|
22
|
+
|
|
23
|
+
/** The non-skipped variant of the runCompact result. */
|
|
24
|
+
export type RanResult = Extract<RunCompactResult, { skipped: false }>;
|
|
25
|
+
|
|
26
|
+
/** Outcome of the pipeline-invocation stage. */
|
|
27
|
+
export type PipelineOutcome =
|
|
28
|
+
| { kind: "return"; view: { messages: AgentMessage[] } | undefined }
|
|
29
|
+
| { kind: "proceed"; ran: RanResult; pressure: number };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Invoke the compaction pipeline with adaptive pressure. Returns a tailed view
|
|
33
|
+
* ("return") when compaction skipped, or "proceed" with the non-skipped result
|
|
34
|
+
* and the computed pressure (consumed by live-trim's critical-over hatch).
|
|
35
|
+
*/
|
|
36
|
+
export function invokePipeline(
|
|
37
|
+
pi: ExtensionAPI,
|
|
38
|
+
runtime: MegaRuntime,
|
|
39
|
+
config: MegaConfig,
|
|
40
|
+
ctx: ExtensionContext,
|
|
41
|
+
opts: {
|
|
42
|
+
messages: AgentMessage[];
|
|
43
|
+
pct: number | null | undefined;
|
|
44
|
+
currentTokens: number;
|
|
45
|
+
tailResult: TailResultFn;
|
|
46
|
+
},
|
|
47
|
+
): PipelineOutcome {
|
|
48
|
+
// Adaptive compression (Fix E): scale compression strength + keepFrom depth
|
|
49
|
+
// with how close we are to the model context limit. Null-safe: when the
|
|
50
|
+
// token-fallback path ran (pct unavailable) use the token-basis pressure
|
|
51
|
+
// (the same basis the runtime `pressure` getter uses for custom/no-window).
|
|
52
|
+
const pressure =
|
|
53
|
+
opts.pct != null
|
|
54
|
+
? pressureFromPct(opts.pct)
|
|
55
|
+
: pressureRatio(opts.currentTokens, runtime.effectiveThreshold);
|
|
56
|
+
const ran = runCompact(pi, runtime, config, ctx, opts.messages, {
|
|
57
|
+
compressionPressure: pressure,
|
|
58
|
+
});
|
|
59
|
+
// D.3: skip paths fall back to replay instead of returning empty.
|
|
60
|
+
// If runCompact skipped and we have a valid trimCache, replay it
|
|
61
|
+
// (free stability win) — otherwise defer to the next event.
|
|
62
|
+
if (ran.skipped) {
|
|
63
|
+
runtime.diagCtxRunSkipped++;
|
|
64
|
+
if (
|
|
65
|
+
runtime.trimCache &&
|
|
66
|
+
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
67
|
+
runtime.trimCache.cut <= opts.messages.length
|
|
68
|
+
) {
|
|
69
|
+
const recent = opts.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.
|
|
70
|
+
runtime.diagLiveTrimFires++;
|
|
71
|
+
runtime.diagLiveTrimReplays++;
|
|
72
|
+
runtime.snapshot(ctx);
|
|
73
|
+
const skipView = [{ ...runtime.trimCache.summaryAgentMsg }, ...recent];
|
|
74
|
+
return { kind: "return", view: opts.tailResult(skipView) ?? { messages: skipView } };
|
|
75
|
+
}
|
|
76
|
+
return { kind: "return", view: opts.tailResult() ?? undefined };
|
|
77
|
+
}
|
|
78
|
+
return { kind: "proceed", ran, pressure };
|
|
79
|
+
}
|