pi-mega-compact 0.21.8 → 0.21.9
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/extensions/dashboard-server/routes-rag-settings-compaction.js +42 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +5 -8
- package/dist/extensions/mega-config.js +12 -0
- package/dist/extensions/mega-events/context-handler/gateCheck.js +51 -1
- package/dist/extensions/mega-events/context-handler/headroom.js +128 -0
- package/dist/extensions/mega-events/context-handler/liveTrim.js +31 -49
- package/dist/extensions/mega-events/context-handler/pipelineRun.js +14 -1
- package/dist/extensions/mega-events/context-handler.js +19 -2
- package/dist/extensions/mega-runtime/dashboard-snapshot.js +1 -0
- package/dist/extensions/mega-runtime/runtime-instrumentation.js +1 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
- package/extensions/dashboard-server/api-contracts/endpoints/types.ts +2 -0
- package/extensions/dashboard-server/routes-rag-settings-compaction.ts +91 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +5 -27
- package/extensions/mega-config-types.ts +25 -0
- package/extensions/mega-config.ts +12 -0
- package/extensions/mega-dashboard.ts +4 -1
- package/extensions/mega-events/context-handler/gateCheck.ts +67 -0
- package/extensions/mega-events/context-handler/headroom.ts +190 -0
- package/extensions/mega-events/context-handler/liveTrim.ts +31 -55
- package/extensions/mega-events/context-handler/pipelineRun.ts +14 -1
- package/extensions/mega-events/context-handler.ts +19 -2
- package/extensions/mega-runtime/dashboard-snapshot.ts +3 -0
- package/extensions/mega-runtime/runtime-instrumentation.ts +4 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-rag-settings-compaction.ts — Compaction SETTINGS.
|
|
3
|
+
*
|
|
4
|
+
* The compaction-group flag inventory, split out of routes-rag-settings-helpers.ts
|
|
5
|
+
* (delegate-shell split per the extensions/ 400-line soft limit) when the
|
|
6
|
+
* v0.21.9 output-headroom flags landed. regression_check.py globs every
|
|
7
|
+
* routes-rag-settings*.ts sibling, so no scanner update is needed on split.
|
|
8
|
+
*
|
|
9
|
+
* PREVENT-011: no `any` type.
|
|
10
|
+
*/
|
|
11
|
+
const boolDirect = (key, label, description, def) => ({
|
|
12
|
+
key,
|
|
13
|
+
label,
|
|
14
|
+
description,
|
|
15
|
+
type: "boolean",
|
|
16
|
+
default: def,
|
|
17
|
+
disabledConvention: false,
|
|
18
|
+
requiresLlm: false,
|
|
19
|
+
});
|
|
20
|
+
const num = (key, label, description, def, min, max, unit) => ({
|
|
21
|
+
key,
|
|
22
|
+
label,
|
|
23
|
+
description,
|
|
24
|
+
type: "number",
|
|
25
|
+
default: def,
|
|
26
|
+
disabledConvention: false,
|
|
27
|
+
requiresLlm: false,
|
|
28
|
+
min,
|
|
29
|
+
max,
|
|
30
|
+
...(unit ? { unit } : {}),
|
|
31
|
+
});
|
|
32
|
+
/** The compaction flags, as one SETTINGS category. */
|
|
33
|
+
export const COMPACTION_SETTINGS = {
|
|
34
|
+
name: "Compaction",
|
|
35
|
+
settings: [
|
|
36
|
+
num("MEGACOMPACT_THRESHOLD_PCT", "Compaction Threshold", "Fraction of the actual model context window at which compaction fires — 0.80 fires at 80% used (leaves 20% free). Applies to any model size; a per-model Model Thresholds row overrides it", 0.8, 0.1, 0.95),
|
|
37
|
+
num("MEGACOMPACT_THRASH_REARM_PCT", "Thrash Re-arm %", "After an ineffective compaction (live window did not shrink), refuse to re-fire until the live window grows by this fraction of the effective threshold. Default 0.10 (10%)", 0.1, 0.01, 0.5),
|
|
38
|
+
boolDirect("MEGACOMPACT_OUTPUT_ERROR_COMPACT", "Output-Error Compact", "When a model response is truncated mid-output (stopReason: 'length'), trip a one-shot forced compaction to free input headroom. Closes the small-context deadlock where the model truncates below the input threshold.", true),
|
|
39
|
+
boolDirect("MEGACOMPACT_OVERFLOW_HEADROOM", "Overflow Headroom Gate", "Fire compaction BEFORE the request overflows the model window — when input tokens + the output reserve + safety margin would exceed the context window — instead of waiting for the percent fire point (which judges only INPUT and never trips on small-window models whose output budget is a large fraction of the window). Percent-based: the reserve scales with the model's own window, so the math holds at every window size (32k…5M). OFF disables this pre-fire check (the gate reverts to input-only judgment); the pair-safe tail-cap hardenings are unconditional safety fixes and remain active.", true),
|
|
40
|
+
num("MEGACOMPACT_OUTPUT_RESERVE_PCT", "Output Reserve %", "FALLBACK output reserve as a fraction of the context window, used only when the model's declared maxTokens is absent or implausible (0, or a models.json sentinel like 1e9/1e38, or >= the window). When maxTokens IS plausible the declared value wins — vLLM-style backends reserve the FULL declared maxTokens. Default 0.30 (30%), clamped 0.10–0.95.", 0.3, 0.1, 0.95),
|
|
41
|
+
],
|
|
42
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VECTOR_CORTEX_SETTINGS } from "./routes-rag-settings-vector-cortex.js";
|
|
2
|
+
import { COMPACTION_SETTINGS } from "./routes-rag-settings-compaction.js";
|
|
2
3
|
// Shorthand builders to keep the inventory terse and unambiguous.
|
|
3
4
|
const boolFlag = (key, label, description, def, requiresLlm = false) => ({
|
|
4
5
|
key,
|
|
@@ -127,14 +128,10 @@ export const SETTINGS = [
|
|
|
127
128
|
num("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", "Embedding Chars per Token", "Estimated characters per token used for embedder chunking size", 4, 1, 32),
|
|
128
129
|
],
|
|
129
130
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
num("MEGACOMPACT_THRASH_REARM_PCT", "Thrash Re-arm %", "After an ineffective compaction (live window did not shrink), refuse to re-fire until the live window grows by this fraction of the effective threshold. Default 0.10 (10%)", 0.1, 0.01, 0.5),
|
|
135
|
-
boolDirect("MEGACOMPACT_OUTPUT_ERROR_COMPACT", "Output-Error Compact", "When a model response is truncated mid-output (stopReason: 'length'), trip a one-shot forced compaction to free input headroom. Closes the small-context deadlock where the model truncates below the input threshold.", true),
|
|
136
|
-
],
|
|
137
|
-
},
|
|
131
|
+
// v0.21.9: compaction group extracted to keep this file under the
|
|
132
|
+
// extensions/ soft limit (delegate-shell split); carries the overflow-
|
|
133
|
+
// headroom + output-reserve flags alongside the pre-existing trio.
|
|
134
|
+
COMPACTION_SETTINGS,
|
|
138
135
|
{
|
|
139
136
|
name: "Three-Way Failback",
|
|
140
137
|
settings: [
|
|
@@ -210,6 +210,18 @@ export function loadConfig() {
|
|
|
210
210
|
// Phase H: output-error catch — trip compaction on a truncated model output
|
|
211
211
|
// (S28 stopReason==='length'). Default ON; OFF byte-identical pre-H.
|
|
212
212
|
outputErrorCompact: envBool("MEGACOMPACT_OUTPUT_ERROR_COMPACT", true),
|
|
213
|
+
// v0.21.9 OUTPUT-HEADROOM GATE: fire compaction BEFORE the request
|
|
214
|
+
// overflows the model window (input + output reserve + margin >= window),
|
|
215
|
+
// not after. Percent-based: the reserve scales with the model's own window
|
|
216
|
+
// so the math holds at every window size (32k…5M). Default ON;
|
|
217
|
+
// OFF = byte-identical pre-v0.21.9 (2026-08-19 32k incident fix).
|
|
218
|
+
overflowHeadroom: envBool("MEGACOMPACT_OVERFLOW_HEADROOM", true),
|
|
219
|
+
// v0.21.9: fallback OUTPUT reserve as a FRACTION of the context window,
|
|
220
|
+
// used when the model's declared maxTokens is absent or implausible
|
|
221
|
+
// (0 / sentinel 1e9/1e38 / >= window). Clamped [0.1, 0.95]; default 0.30.
|
|
222
|
+
// When maxTokens IS plausible the declared value wins (vLLM reserves the
|
|
223
|
+
// full maxTokens) — this fraction is only the fallback.
|
|
224
|
+
outputReservePct: clamp(envFlag("MEGACOMPACT_OUTPUT_RESERVE_PCT", 0.3), 0.1, 0.95),
|
|
213
225
|
windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
|
|
214
226
|
recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
|
|
215
227
|
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveModelThreshold, DEFAULT_SAFETY_MARGIN_PCT, DEFAULT_FIRE_POINT_PCT, } from "../../../src/store/sqlite.js";
|
|
2
2
|
import { autoCompactCheck } from "../../../src/compact.js";
|
|
3
3
|
import { isThrashBlockedFor } from "./thrashGuard.js";
|
|
4
|
+
import { resolveOutputReserve } from "./headroom.js";
|
|
4
5
|
/**
|
|
5
6
|
* 3WF-2 ThrashGuard consult — refuse to fire a NEW compaction while the guard
|
|
6
7
|
* is armed. After an ineffective compaction (the live window did not shrink),
|
|
@@ -20,11 +21,18 @@ import { isThrashBlockedFor } from "./thrashGuard.js";
|
|
|
20
21
|
* Umbrella OFF ⇒ always false (byte-identical to v0.20.83). Non-fatal: a store
|
|
21
22
|
* read error returns false — never refuse compaction on a store fault.
|
|
22
23
|
*/
|
|
23
|
-
export function thrashGuardBlocks(runtime, config, currentTokens) {
|
|
24
|
+
export function thrashGuardBlocks(runtime, config, currentTokens, headroomExceeded) {
|
|
24
25
|
if (!config.threeWayFailback)
|
|
25
26
|
return false;
|
|
26
27
|
if (currentTokens == null)
|
|
27
28
|
return false;
|
|
29
|
+
// v0.21.9: an overflow-bound fire (headroomExceeded) is EXEMPT from the
|
|
30
|
+
// thrash guard. The guard exists to stop wasted re-compaction when the
|
|
31
|
+
// window refuses to shrink; but an overflowed request is not "wasted work"
|
|
32
|
+
// — it is the model about to 400. Blocking that fire reproduces the
|
|
33
|
+
// 2026-08-19 32k deadlock (compact never → request > window → error loop).
|
|
34
|
+
if (headroomExceeded)
|
|
35
|
+
return false;
|
|
28
36
|
return isThrashBlockedFor(runtime, currentTokens, runtime.currentStateDir);
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
@@ -67,6 +75,48 @@ export function evaluateGate(runtime, config, opts) {
|
|
|
67
75
|
runtime.diagCtxOutputErrorTrip++;
|
|
68
76
|
return { kind: "proceed", perModelThreshold };
|
|
69
77
|
}
|
|
78
|
+
// v0.21.9 OUTPUT-HEADROOM GATE (the root-cause fix for the 32k truncation
|
|
79
|
+
// loop). The percent/token fire points above judge only INPUT utilization
|
|
80
|
+
// (tier% of the window), but a request's budget is
|
|
81
|
+
// input tokens + the model's output budget + safety margin.
|
|
82
|
+
// On a small-window model with a large maxTokens (the user's 32k/20k
|
|
83
|
+
// GLM-4.7), the request overflows at ~32% INPUT (21.4k + 20k > 32.768k) —
|
|
84
|
+
// long before any percent gate fires → provider 400 "request exceeds the
|
|
85
|
+
// available context size" every turn → the poisoned-error loop. Phase H only
|
|
86
|
+
// reacts to stopReason 'length' (mid-output truncation); a pre-output 400
|
|
87
|
+
// never arms it, so "compact never". This check fires the compaction
|
|
88
|
+
// BEFORE the overflow instead of after.
|
|
89
|
+
//
|
|
90
|
+
// PERCENT-BASED (LTS invariant — must work at every window size: 32k, 64k,
|
|
91
|
+
// 200k, 1M, 5M): the reserve is a FRACTION of the model's own window via
|
|
92
|
+
// resolveOutputReserve (plausible declared maxTokens wins, else
|
|
93
|
+
// clamp(MEGACOMPACT_OUTPUT_RESERVE_PCT, 10–95%) × window). Same math, any
|
|
94
|
+
// size. window <= 0 (unknown) ⇒ deferred (never guess a window), matching
|
|
95
|
+
// the effectiveThresholdImpl Phase-C invariant. Gated on
|
|
96
|
+
// config.overflowHeadroom (default ON; OFF = byte-identical pre-v0.21.9).
|
|
97
|
+
// Thrash-guard exemption: headroomExceeded rides along on the proceed so the
|
|
98
|
+
// handler's thrash consult never refuses an overflow-bound fire (see
|
|
99
|
+
// thrashGuardBlocks above) — an overflowed session is unrecoverable, so a
|
|
100
|
+
// wasted re-fire is always the better outcome (2026-08-19 incident).
|
|
101
|
+
if (config.overflowHeadroom &&
|
|
102
|
+
runtime.lastCtxWindow > 0 &&
|
|
103
|
+
Number.isFinite(currentTokens) &&
|
|
104
|
+
currentTokens > 0) {
|
|
105
|
+
const { reserveTokens, fallbackUsed } = resolveOutputReserve(runtime.lastCtxWindow, runtime.currentModel?.maxTokens ?? 0, config.outputReservePct);
|
|
106
|
+
const headroomMargin = Math.ceil(runtime.lastCtxWindow * (perModelThreshold.safetyMarginPct / 100));
|
|
107
|
+
if (currentTokens + reserveTokens + headroomMargin >= runtime.lastCtxWindow) {
|
|
108
|
+
runtime.diagCtxHeadroomTrip++;
|
|
109
|
+
runtime.logger.info("gate-headroom-trip", {
|
|
110
|
+
sessionId: runtime.rt.sessionId,
|
|
111
|
+
currentTokens,
|
|
112
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
113
|
+
reserveTokens,
|
|
114
|
+
fallbackUsed,
|
|
115
|
+
marginPct: perModelThreshold.safetyMarginPct,
|
|
116
|
+
});
|
|
117
|
+
return { kind: "proceed", perModelThreshold, headroomExceeded: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
70
120
|
// S29 FAST GATE: `custom` (absolute MEGACOMPACT_THRESHOLD_TOKENS,
|
|
71
121
|
// tierPct null) is an explicit opt-out of percent scaling — it keeps the
|
|
72
122
|
// token gate. When pct is unavailable (window unknown / a model that
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { estimateBlockTokens, estimateMessageTokens } from "../../../src/tokens.js";
|
|
2
|
+
import { messageContentText } from "./messageText.js";
|
|
3
|
+
/**
|
|
4
|
+
* The model's declared maxTokens is only trusted as the output budget when it
|
|
5
|
+
* is plausible. models.json carries sentinel junk for some entries (1e9,
|
|
6
|
+
* 1e38, "unlimited"), and some providers report 0/absent. A declared budget
|
|
7
|
+
* above this FRACTION of the window is implausible — fall back to the
|
|
8
|
+
* configured fraction so a 200k/1e9 model doesn't compute a negative budget
|
|
9
|
+
* and silently disable the cap (the pre-v0.21.9 bug). Percent-based: holds at
|
|
10
|
+
* every window size.
|
|
11
|
+
*
|
|
12
|
+
* WHY 0.95 AND NOT LOWER: vLLM-style backends reject a request when
|
|
13
|
+
* `input + max_tokens > context window` — they reserve the model's FULL
|
|
14
|
+
* declared maxTokens, not a fraction of it. The user's own GLM-4.7 entry is
|
|
15
|
+
* 32000/20000 (62.5%); a 0.6 cutoff rejected that REAL config as
|
|
16
|
+
* "implausible" and fell back to a 30% reserve (9600) while the backend
|
|
17
|
+
* reserved the full 20000 — the gate would keep firing late and the
|
|
18
|
+
* post-compact tail would still overflow (2026-08-19 incident, attempt #6).
|
|
19
|
+
* A declared budget is plausible up to just below the WHOLE window; anything
|
|
20
|
+
* at/above the window (or the 1e9/1e38 sentinels) is junk.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_OUTPUT_PLAUSIBLE_FRACTION = 0.95;
|
|
23
|
+
/** Bounds for the fallback reserve fraction (MEGACOMPACT_OUTPUT_RESERVE_PCT). */
|
|
24
|
+
export const OUTPUT_RESERVE_PCT_MIN = 0.1;
|
|
25
|
+
export const OUTPUT_RESERVE_PCT_MAX = 0.95;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the output reserve (tokens) for a model window.
|
|
28
|
+
*
|
|
29
|
+
* - window <= 0 (unknown) → { reserveTokens: 0, fallbackUsed: false }; every
|
|
30
|
+
* consumer is guarded on window > 0 and defers (never guesses a window).
|
|
31
|
+
* - maxTokens plausible (0 < maxTokens <= 95% of the window) → maxTokens —
|
|
32
|
+
* vLLM-style backends reserve the FULL declared maxTokens, so the reserve
|
|
33
|
+
* must equal it, not a fraction of it.
|
|
34
|
+
* - otherwise → clamp(outputReservePct, 0.1, 0.95) × window.
|
|
35
|
+
*
|
|
36
|
+
* `outputReservePct` is config.outputReservePct (already env-clamped at load,
|
|
37
|
+
* re-clamped here for defense against direct callers).
|
|
38
|
+
*/
|
|
39
|
+
export function resolveOutputReserve(ctxWindow, maxTokens, outputReservePct) {
|
|
40
|
+
if (!Number.isFinite(ctxWindow) || ctxWindow <= 0) {
|
|
41
|
+
return { reserveTokens: 0, fallbackUsed: false };
|
|
42
|
+
}
|
|
43
|
+
const plausible = Number.isFinite(maxTokens) &&
|
|
44
|
+
maxTokens > 0 &&
|
|
45
|
+
maxTokens <= ctxWindow * MAX_OUTPUT_PLAUSIBLE_FRACTION;
|
|
46
|
+
if (plausible)
|
|
47
|
+
return { reserveTokens: Math.round(maxTokens), fallbackUsed: false };
|
|
48
|
+
const pct = Math.min(OUTPUT_RESERVE_PCT_MAX, Math.max(OUTPUT_RESERVE_PCT_MIN, Number.isFinite(outputReservePct) ? outputReservePct : 0.3));
|
|
49
|
+
return { reserveTokens: Math.ceil(ctxWindow * pct), fallbackUsed: true };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Pair-safe front-drop for the live-trim tail cap. Drops OLDEST messages from
|
|
53
|
+
* the front of `recentRaw` until the remaining tail fits
|
|
54
|
+
* `ctxWindow − outputReserve − safetyMargin − summaryTokens`, then advances
|
|
55
|
+
* the start index past any leading toolResult messages so the preserved tail
|
|
56
|
+
* never begins on an orphaned toolResult (PREVENT-PI-002: a toolCall/toolResult
|
|
57
|
+
* pair must not be split). Never returns an empty tail — the final message is
|
|
58
|
+
* always kept so the agent can respond.
|
|
59
|
+
*
|
|
60
|
+
* v0.21.9 hardenings over the pre-v0.21.9 inline cap in liveTrim.ts:
|
|
61
|
+
* 1. BUDGET FLOOR — when the reserve exceeds the window (implausible maxTokens
|
|
62
|
+
* made budget <= 0) the old block silently skipped the cap entirely and an
|
|
63
|
+
* oversized tail sailed past the window. Now the reserve is clamped (via
|
|
64
|
+
* resolveOutputReserve) to a fraction of the window, so a floor budget
|
|
65
|
+
* always exists. If even ONE message exceeds the floor budget we keep only
|
|
66
|
+
* the final message — the agent's last turn is the one thing the model
|
|
67
|
+
* must always see.
|
|
68
|
+
* 2. TOOL-PAIR SAFETY — the old front-drop could land between a toolCall and
|
|
69
|
+
* its toolResult, splitting the pair.
|
|
70
|
+
*
|
|
71
|
+
* Pure: returns { recent, dropped } without touching the input array.
|
|
72
|
+
*/
|
|
73
|
+
export function applyTailCap(opts) {
|
|
74
|
+
const { recentRaw, summaryTokens, ctxWindow, outputReservePct } = opts;
|
|
75
|
+
if (ctxWindow <= 0 || recentRaw.length <= 1) {
|
|
76
|
+
return { recent: [...recentRaw], dropped: 0 };
|
|
77
|
+
}
|
|
78
|
+
const msgTokens = opts.messageTokens && opts.messageTokens.length === recentRaw.length
|
|
79
|
+
? opts.messageTokens
|
|
80
|
+
: null;
|
|
81
|
+
const { reserveTokens } = resolveOutputReserve(ctxWindow, opts.maxOutputTokens, outputReservePct);
|
|
82
|
+
const safetyMargin = Math.ceil(ctxWindow * (Math.max(0, opts.safetyMarginPct) / 100));
|
|
83
|
+
// Budget floor: never negative. An implausible reserve (clamped above to
|
|
84
|
+
// <= 95% of the window) plus margin + summary can still exceed the window
|
|
85
|
+
// on tiny summaries-free edges; the floor keeps the cap alive with a small
|
|
86
|
+
// positive budget instead of disabling it (pre-v0.21.9 behavior).
|
|
87
|
+
const budget = Math.max(1, ctxWindow - reserveTokens - safetyMargin - Math.max(0, summaryTokens));
|
|
88
|
+
let start = 0;
|
|
89
|
+
let tailTokens = 0;
|
|
90
|
+
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
91
|
+
tailTokens +=
|
|
92
|
+
msgTokens != null
|
|
93
|
+
? Math.max(0, msgTokens[i])
|
|
94
|
+
: estimateMessageTokens({ text: messageContentText(recentRaw[i]) });
|
|
95
|
+
if (tailTokens > budget) {
|
|
96
|
+
// Keep from i+1 onward; never drop below the FINAL message.
|
|
97
|
+
start = Math.min(i + 1, recentRaw.length - 1);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// PREVENT-PI-002: never begin the preserved tail on an orphaned toolResult —
|
|
102
|
+
// its toolCall was dropped by the front-cut above. Advance past consecutive
|
|
103
|
+
// toolResults; the pair stays intact or drops whole.
|
|
104
|
+
while (start < recentRaw.length - 1 &&
|
|
105
|
+
recentRaw[start].role === "toolResult") {
|
|
106
|
+
start++;
|
|
107
|
+
}
|
|
108
|
+
return { recent: recentRaw.slice(start), dropped: start };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* v0.21.9: re-cap a REPLAYED trim tail (D.2 in context-handler.ts, D.3 in
|
|
112
|
+
* pipelineRun.ts). The replay paths return the cached trim view verbatim —
|
|
113
|
+
* which bypasses the fire-time tail cap. A model switch mid-epoch can shrink
|
|
114
|
+
* the window, leaving a replayed tail that fit the OLD window overflowing the
|
|
115
|
+
* NEW one. Re-runs applyTailCap against the CURRENT window with the margin
|
|
116
|
+
* stored at fire time (trimCache.safetyMarginPct), so the replayed view never
|
|
117
|
+
* exceeds what the gate would allow. Pure — no runtime dependency.
|
|
118
|
+
*/
|
|
119
|
+
export function recapReplayedTail(opts) {
|
|
120
|
+
return applyTailCap({
|
|
121
|
+
recentRaw: opts.recentRaw,
|
|
122
|
+
summaryTokens: estimateBlockTokens(messageContentText(opts.summaryAgentMsg)),
|
|
123
|
+
ctxWindow: opts.ctxWindow,
|
|
124
|
+
maxOutputTokens: opts.maxOutputTokens,
|
|
125
|
+
outputReservePct: opts.outputReservePct,
|
|
126
|
+
safetyMarginPct: opts.safetyMarginPct,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { estimateBlockTokens
|
|
1
|
+
import { estimateBlockTokens } from "../../../src/tokens.js";
|
|
2
2
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../../mega-trim.js";
|
|
3
|
-
import {
|
|
3
|
+
import { applyTailCap } from "./headroom.js";
|
|
4
4
|
/**
|
|
5
5
|
* Reconstruct the live-trim window (summary + recent anchor) for this LLM
|
|
6
6
|
* call. Returns the tailed view, or undefined when no trim is safe this call.
|
|
@@ -79,56 +79,33 @@ export function buildLiveTrimView(runtime, config, ctx, opts) {
|
|
|
79
79
|
// but has NO token cap, so a 2-message tail of two 80K bash outputs sails
|
|
80
80
|
// right past the window.
|
|
81
81
|
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
82
|
+
// v0.21.9: the reserve + front-drop now lives in headroom.ts (single
|
|
83
|
+
// source shared with the gate's pre-fire headroom check and the D.2/D.3
|
|
84
|
+
// replay paths): (a) percent-based reserve — plausible declared maxTokens
|
|
85
|
+
// wins, else clamp(MEGACOMPACT_OUTPUT_RESERVE_PCT, 10–95%) × window — so
|
|
86
|
+
// the math is identical at any window size and a sentinel maxTokens
|
|
87
|
+
// (1e9/1e38) can no longer drive the budget negative and silently
|
|
88
|
+
// disable the cap; (b) budget floor (max(1, …)) so the cap stays active
|
|
89
|
+
// on every window; (c) pair-safe front-drop — the preserved tail never
|
|
90
|
+
// begins on an orphaned toolResult (PREVENT-PI-002).
|
|
88
91
|
const ctxWindow = runtime.lastCtxWindow;
|
|
89
92
|
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
90
93
|
const modelThreshold = perModelThreshold;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// remaining tail fits. Use the AgentMessage→engine-text estimate via
|
|
107
|
-
// messageContentText (already imported) + estimateMessageTokens.
|
|
108
|
-
let tailTokens = 0;
|
|
109
|
-
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
110
|
-
const m = recentRaw[i];
|
|
111
|
-
tailTokens += estimateMessageTokens({
|
|
112
|
-
text: messageContentText(m),
|
|
113
|
-
});
|
|
114
|
-
if (tailTokens > budget) {
|
|
115
|
-
// Keep from i+1 onward; but never fewer than the final message.
|
|
116
|
-
const startIdx = Math.min(i + 1, recentRaw.length - 1);
|
|
117
|
-
if (startIdx > 0) {
|
|
118
|
-
recent = recentRaw.slice(startIdx);
|
|
119
|
-
runtime.logger.warn("live-trim-tail-cap", {
|
|
120
|
-
sessionId: runtime.rt.sessionId,
|
|
121
|
-
dropped: startIdx,
|
|
122
|
-
tailTokens,
|
|
123
|
-
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
124
|
-
budget,
|
|
125
|
-
ctxWindow,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
94
|
+
const { recent, dropped } = applyTailCap({
|
|
95
|
+
recentRaw,
|
|
96
|
+
summaryTokens: estimateBlockTokens(summaryMsg.text),
|
|
97
|
+
ctxWindow,
|
|
98
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
99
|
+
outputReservePct: config.outputReservePct,
|
|
100
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
101
|
+
});
|
|
102
|
+
if (dropped > 0) {
|
|
103
|
+
runtime.logger.warn("live-trim-tail-cap", {
|
|
104
|
+
sessionId: runtime.rt.sessionId,
|
|
105
|
+
dropped,
|
|
106
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
107
|
+
ctxWindow,
|
|
108
|
+
});
|
|
132
109
|
}
|
|
133
110
|
// v0.8.6: cache the trim view so subsequent gated calls in this epoch
|
|
134
111
|
// replay it verbatim (stabilizing the KV-cache prefix) instead of
|
|
@@ -152,6 +129,11 @@ export function buildLiveTrimView(runtime, config, ctx, opts) {
|
|
|
152
129
|
summaryAgentMsg,
|
|
153
130
|
ctxPct: pct ?? null,
|
|
154
131
|
ctxTokens: currentTokens,
|
|
132
|
+
// v0.21.9: the D.2/D.3 replay paths re-cap the replayed tail against
|
|
133
|
+
// the CURRENT window (a model switch can change it mid-epoch). The
|
|
134
|
+
// margin % used at fire time is stored alongside so the replay uses
|
|
135
|
+
// the same reserve math as the fire that built the view.
|
|
136
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
155
137
|
};
|
|
156
138
|
runtime.snapshot(ctx);
|
|
157
139
|
// DIAG (team-run relief): confirm the live trim actually fires + how big
|
|
@@ -2,6 +2,7 @@ import { runCompact } from "../../mega-pipeline.js";
|
|
|
2
2
|
import { pressureFromPct, pressureRatio } from "../../mega-config.js";
|
|
3
3
|
import { recordCompactLatency } from "../../mega-runtime/vc-observer.js";
|
|
4
4
|
import { decideLivePath } from "../../mega-runtime/vector-cortex-live.js";
|
|
5
|
+
import { recapReplayedTail } from "./headroom.js";
|
|
5
6
|
import { defaultClock } from "../../../src/vector-cortex/rollout/gate.js";
|
|
6
7
|
import { VC5C_ENABLED } from "../../../src/config/vector-cortex.js";
|
|
7
8
|
/**
|
|
@@ -72,7 +73,19 @@ export function invokePipeline(pi, runtime, config, ctx, opts) {
|
|
|
72
73
|
if (runtime.trimCache &&
|
|
73
74
|
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
74
75
|
runtime.trimCache.cut <= opts.messages.length) {
|
|
75
|
-
const
|
|
76
|
+
const recentRaw = 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.
|
|
77
|
+
// v0.21.9: RE-CAP the replayed tail against the CURRENT window —
|
|
78
|
+
// the D.3 skip-replay bypasses the fire-time tail cap exactly like
|
|
79
|
+
// D.2; a model switch mid-epoch can shrink the window below what
|
|
80
|
+
// the cached view was built for. No-op when the tail already fits.
|
|
81
|
+
const { recent } = recapReplayedTail({
|
|
82
|
+
recentRaw,
|
|
83
|
+
summaryAgentMsg: runtime.trimCache.summaryAgentMsg,
|
|
84
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
85
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
86
|
+
outputReservePct: config.outputReservePct,
|
|
87
|
+
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
88
|
+
});
|
|
76
89
|
runtime.diagLiveTrimFires++;
|
|
77
90
|
runtime.diagLiveTrimReplays++;
|
|
78
91
|
runtime.snapshot(ctx);
|
|
@@ -9,6 +9,7 @@ import { evaluateGate, thrashGuardBlocks } from "./context-handler/gateCheck.js"
|
|
|
9
9
|
import { markCompactionFired, evaluatePendingReduction, } from "./context-handler/thrashGuard.js";
|
|
10
10
|
import { invokePipeline } from "./context-handler/pipelineRun.js";
|
|
11
11
|
import { buildLiveTrimView } from "./context-handler/liveTrim.js";
|
|
12
|
+
import { recapReplayedTail } from "./context-handler/headroom.js";
|
|
12
13
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
13
14
|
export function registerContextHandler(pi, runtime, config) {
|
|
14
15
|
// ---- Auto-trigger: live trim (compact and continue) + native durable ----
|
|
@@ -139,7 +140,23 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
139
140
|
: currentTokens - (runtime.trimCache.ctxTokens ?? 0) >=
|
|
140
141
|
runtime.effectiveThreshold * 0.5;
|
|
141
142
|
if (!grewEnough) {
|
|
142
|
-
const
|
|
143
|
+
const recentRaw = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized once by computeLiveTrimCut (src/boundary.ts) and replayed verbatim; the transcript only grows within an epoch (cache is cleared on durable truncation), so the preserved run still starts on a toolPair-safe index.
|
|
144
|
+
// v0.21.9: RE-CAP the replayed tail against the CURRENT window.
|
|
145
|
+
// Replay returns the cached view verbatim, which bypasses the
|
|
146
|
+
// fire-time tail cap — a model switch mid-epoch can shrink the
|
|
147
|
+
// window and leave a replayed tail that fit the OLD window
|
|
148
|
+
// overflowing the NEW one. Same reserve math as the fire
|
|
149
|
+
// (margin % stored in the cache at fire time); no-op when the
|
|
150
|
+
// tail already fits. Pair-safe (applyTailCap advances past any
|
|
151
|
+
// leading toolResult its front-drop exposes).
|
|
152
|
+
const { recent } = recapReplayedTail({
|
|
153
|
+
recentRaw,
|
|
154
|
+
summaryAgentMsg: runtime.trimCache.summaryAgentMsg,
|
|
155
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
156
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
157
|
+
outputReservePct: config.outputReservePct,
|
|
158
|
+
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
159
|
+
});
|
|
143
160
|
runtime.diagLiveTrimFires++; // trim view returned this call (replay counts as a fire)
|
|
144
161
|
runtime.diagLiveTrimReplays++;
|
|
145
162
|
runtime.snapshot(ctx);
|
|
@@ -159,7 +176,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
159
176
|
// invokePipeline (the real fire point), so it covers the percent + token
|
|
160
177
|
// gate paths alike. Umbrella OFF ⇒ never blocks (byte-identical). Returns
|
|
161
178
|
// the tailed view so a staged recall block still rides along.
|
|
162
|
-
if (thrashGuardBlocks(runtime, config, currentTokens)) {
|
|
179
|
+
if (thrashGuardBlocks(runtime, config, currentTokens, gate.headroomExceeded)) {
|
|
163
180
|
runtime.diagCtxFastGate++;
|
|
164
181
|
runtime.snapshot(ctx);
|
|
165
182
|
return tailResult() ?? undefined;
|
|
@@ -128,6 +128,7 @@ export function buildDashboardSnapshot(ctx) {
|
|
|
128
128
|
ctxFastGate: ctx.diagCtxFastGate,
|
|
129
129
|
liveTrimFires: ctx.diagLiveTrimFires,
|
|
130
130
|
liveTrimReplays: ctx.diagLiveTrimReplays,
|
|
131
|
+
headroomTrips: ctx.diagCtxHeadroomTrip,
|
|
131
132
|
},
|
|
132
133
|
retries: {
|
|
133
134
|
errorRetryCount: ctx.errorRetryCount,
|
|
@@ -23,6 +23,7 @@ export class RuntimeInstrumentation {
|
|
|
23
23
|
diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
|
|
24
24
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
25
25
|
diagCtxOutputErrorTrip = 0; // Phase H: output-error catch tripped a forced compaction
|
|
26
|
+
diagCtxHeadroomTrip = 0; // v0.21.9: output-headroom gate tripped a pre-overflow compaction
|
|
26
27
|
// Context health instrumentation (v0.12): rolling ring buffers for
|
|
27
28
|
// drift detection + cache poison Layer 1 hash baseline.
|
|
28
29
|
recentTurnEmbeddings = [];
|
|
@@ -94,6 +94,7 @@ export function snapshotImpl(self, ctx) {
|
|
|
94
94
|
diagCtxFastGate: self.diagCtxFastGate,
|
|
95
95
|
diagLiveTrimFires: self.diagLiveTrimFires,
|
|
96
96
|
diagLiveTrimReplays: self.diagLiveTrimReplays,
|
|
97
|
+
diagCtxHeadroomTrip: self.diagCtxHeadroomTrip,
|
|
97
98
|
errorRetryCount: self.rt.errorRetryCount,
|
|
98
99
|
consecutiveErrors: self.rt.consecutiveErrors,
|
|
99
100
|
ERROR_RETRY_MAX_CONSECUTIVE: self.config.maxConsecutiveErrors,
|
|
@@ -303,6 +303,8 @@ export interface PerfDiag {
|
|
|
303
303
|
readonly liveTrimFires: number;
|
|
304
304
|
/** Number of live trim replays. */
|
|
305
305
|
readonly liveTrimReplays: number;
|
|
306
|
+
/** v0.21.9: output-headroom gate trips (pre-overflow compaction fires). */
|
|
307
|
+
readonly headroomTrips: number;
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-rag-settings-compaction.ts — Compaction SETTINGS.
|
|
3
|
+
*
|
|
4
|
+
* The compaction-group flag inventory, split out of routes-rag-settings-helpers.ts
|
|
5
|
+
* (delegate-shell split per the extensions/ 400-line soft limit) when the
|
|
6
|
+
* v0.21.9 output-headroom flags landed. regression_check.py globs every
|
|
7
|
+
* routes-rag-settings*.ts sibling, so no scanner update is needed on split.
|
|
8
|
+
*
|
|
9
|
+
* PREVENT-011: no `any` type.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SettingSpec, SettingGroup } from "./routes-rag-settings-types.js";
|
|
13
|
+
|
|
14
|
+
const boolDirect = (
|
|
15
|
+
key: string,
|
|
16
|
+
label: string,
|
|
17
|
+
description: string,
|
|
18
|
+
def: boolean,
|
|
19
|
+
): SettingSpec => ({
|
|
20
|
+
key,
|
|
21
|
+
label,
|
|
22
|
+
description,
|
|
23
|
+
type: "boolean",
|
|
24
|
+
default: def,
|
|
25
|
+
disabledConvention: false,
|
|
26
|
+
requiresLlm: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const num = (
|
|
30
|
+
key: string,
|
|
31
|
+
label: string,
|
|
32
|
+
description: string,
|
|
33
|
+
def: number,
|
|
34
|
+
min: number,
|
|
35
|
+
max: number,
|
|
36
|
+
unit?: string,
|
|
37
|
+
): SettingSpec => ({
|
|
38
|
+
key,
|
|
39
|
+
label,
|
|
40
|
+
description,
|
|
41
|
+
type: "number",
|
|
42
|
+
default: def,
|
|
43
|
+
disabledConvention: false,
|
|
44
|
+
requiresLlm: false,
|
|
45
|
+
min,
|
|
46
|
+
max,
|
|
47
|
+
...(unit ? { unit } : {}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The compaction flags, as one SETTINGS category. */
|
|
51
|
+
export const COMPACTION_SETTINGS: SettingGroup = {
|
|
52
|
+
name: "Compaction",
|
|
53
|
+
settings: [
|
|
54
|
+
num(
|
|
55
|
+
"MEGACOMPACT_THRESHOLD_PCT",
|
|
56
|
+
"Compaction Threshold",
|
|
57
|
+
"Fraction of the actual model context window at which compaction fires — 0.80 fires at 80% used (leaves 20% free). Applies to any model size; a per-model Model Thresholds row overrides it",
|
|
58
|
+
0.8,
|
|
59
|
+
0.1,
|
|
60
|
+
0.95,
|
|
61
|
+
),
|
|
62
|
+
num(
|
|
63
|
+
"MEGACOMPACT_THRASH_REARM_PCT",
|
|
64
|
+
"Thrash Re-arm %",
|
|
65
|
+
"After an ineffective compaction (live window did not shrink), refuse to re-fire until the live window grows by this fraction of the effective threshold. Default 0.10 (10%)",
|
|
66
|
+
0.1,
|
|
67
|
+
0.01,
|
|
68
|
+
0.5,
|
|
69
|
+
),
|
|
70
|
+
boolDirect(
|
|
71
|
+
"MEGACOMPACT_OUTPUT_ERROR_COMPACT",
|
|
72
|
+
"Output-Error Compact",
|
|
73
|
+
"When a model response is truncated mid-output (stopReason: 'length'), trip a one-shot forced compaction to free input headroom. Closes the small-context deadlock where the model truncates below the input threshold.",
|
|
74
|
+
true,
|
|
75
|
+
),
|
|
76
|
+
boolDirect(
|
|
77
|
+
"MEGACOMPACT_OVERFLOW_HEADROOM",
|
|
78
|
+
"Overflow Headroom Gate",
|
|
79
|
+
"Fire compaction BEFORE the request overflows the model window — when input tokens + the output reserve + safety margin would exceed the context window — instead of waiting for the percent fire point (which judges only INPUT and never trips on small-window models whose output budget is a large fraction of the window). Percent-based: the reserve scales with the model's own window, so the math holds at every window size (32k…5M). OFF disables this pre-fire check (the gate reverts to input-only judgment); the pair-safe tail-cap hardenings are unconditional safety fixes and remain active.",
|
|
80
|
+
true,
|
|
81
|
+
),
|
|
82
|
+
num(
|
|
83
|
+
"MEGACOMPACT_OUTPUT_RESERVE_PCT",
|
|
84
|
+
"Output Reserve %",
|
|
85
|
+
"FALLBACK output reserve as a fraction of the context window, used only when the model's declared maxTokens is absent or implausible (0, or a models.json sentinel like 1e9/1e38, or >= the window). When maxTokens IS plausible the declared value wins — vLLM-style backends reserve the FULL declared maxTokens. Default 0.30 (30%), clamped 0.10–0.95.",
|
|
86
|
+
0.3,
|
|
87
|
+
0.1,
|
|
88
|
+
0.95,
|
|
89
|
+
),
|
|
90
|
+
],
|
|
91
|
+
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { SettingSpec, SettingGroup } from "./routes-rag-settings-types.js";
|
|
11
11
|
import { VECTOR_CORTEX_SETTINGS } from "./routes-rag-settings-vector-cortex.js";
|
|
12
|
+
import { COMPACTION_SETTINGS } from "./routes-rag-settings-compaction.js";
|
|
12
13
|
|
|
13
14
|
export type { SettingSpec } from "./routes-rag-settings-types.js";
|
|
14
15
|
|
|
@@ -275,33 +276,10 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
|
|
|
275
276
|
num("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", "Embedding Chars per Token", "Estimated characters per token used for embedder chunking size", 4, 1, 32),
|
|
276
277
|
],
|
|
277
278
|
},
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"MEGACOMPACT_THRESHOLD_PCT",
|
|
283
|
-
"Compaction Threshold",
|
|
284
|
-
"Fraction of the actual model context window at which compaction fires — 0.80 fires at 80% used (leaves 20% free). Applies to any model size; a per-model Model Thresholds row overrides it",
|
|
285
|
-
0.8,
|
|
286
|
-
0.1,
|
|
287
|
-
0.95,
|
|
288
|
-
),
|
|
289
|
-
num(
|
|
290
|
-
"MEGACOMPACT_THRASH_REARM_PCT",
|
|
291
|
-
"Thrash Re-arm %",
|
|
292
|
-
"After an ineffective compaction (live window did not shrink), refuse to re-fire until the live window grows by this fraction of the effective threshold. Default 0.10 (10%)",
|
|
293
|
-
0.1,
|
|
294
|
-
0.01,
|
|
295
|
-
0.5,
|
|
296
|
-
),
|
|
297
|
-
boolDirect(
|
|
298
|
-
"MEGACOMPACT_OUTPUT_ERROR_COMPACT",
|
|
299
|
-
"Output-Error Compact",
|
|
300
|
-
"When a model response is truncated mid-output (stopReason: 'length'), trip a one-shot forced compaction to free input headroom. Closes the small-context deadlock where the model truncates below the input threshold.",
|
|
301
|
-
true,
|
|
302
|
-
),
|
|
303
|
-
],
|
|
304
|
-
},
|
|
279
|
+
// v0.21.9: compaction group extracted to keep this file under the
|
|
280
|
+
// extensions/ soft limit (delegate-shell split); carries the overflow-
|
|
281
|
+
// headroom + output-reserve flags alongside the pre-existing trio.
|
|
282
|
+
COMPACTION_SETTINGS,
|
|
305
283
|
{
|
|
306
284
|
name: "Three-Way Failback",
|
|
307
285
|
settings: [
|
|
@@ -141,6 +141,31 @@ export interface MegaConfig {
|
|
|
141
141
|
* gate never fires → "compact never" → every subsequent response truncates
|
|
142
142
|
* too). Default ON; OFF (=0/`=false`) = byte-identical pre-H. */
|
|
143
143
|
outputErrorCompact: boolean;
|
|
144
|
+
/** v0.21.9: output-headroom gate. Fire compaction BEFORE the request
|
|
145
|
+
* overflows the model window — when
|
|
146
|
+
* `currentTokens + outputReserve + safetyMargin >= contextWindow` —
|
|
147
|
+
* instead of waiting for the percent/token fire point (which judges only
|
|
148
|
+
* INPUT and never trips on small-window models whose output reserve is a
|
|
149
|
+
* large fraction of the window: a 32k window with a 20k maxTokens
|
|
150
|
+
* overflows at ~37% INPUT). Percent-based by construction: the reserve is
|
|
151
|
+
* a fraction of the model's own window, so the math is identical at every
|
|
152
|
+
* window size (32k, 64k, 200k, 1M, 5M). Default ON; OFF (=0/`=false`)
|
|
153
|
+
* disables ONLY the pre-fire gate check (the gate reverts to the
|
|
154
|
+
* input-only pre-v0.21.9 judgment). NOTE: the shared tail-cap hardenings
|
|
155
|
+
* this fix introduced — pair-safe front-drop (the pre-v0.21.9 cap could
|
|
156
|
+
* split a toolCall/toolResult pair, PREVENT-PI-002) and the budget floor
|
|
157
|
+
* (the old cap silently disabled itself when the reserve exceeded the
|
|
158
|
+
* window) — are UNCONDITIONAL guardrail fixes and apply regardless of
|
|
159
|
+
* this flag. Headroom-triggered fires are EXEMPT from the ThrashGuard
|
|
160
|
+
* (an overflowed session is unrecoverable). */
|
|
161
|
+
overflowHeadroom: boolean;
|
|
162
|
+
/** v0.21.9: fallback output reserve as a fraction of the context window
|
|
163
|
+
* when the model's declared maxTokens is absent or implausible (0, or the
|
|
164
|
+
* models.json sentinels 1e9/1e38, or >= the window). Clamped [0.1, 0.95];
|
|
165
|
+
* default 0.30 (30% of the window). When maxTokens is plausible the
|
|
166
|
+
* declared value wins — vLLM-style backends reserve the FULL declared
|
|
167
|
+
* maxTokens against the context window, so the reserve must match it. */
|
|
168
|
+
outputReservePct: number;
|
|
144
169
|
/** Inline-dedupe recalled checkpoints against the live window (Fix C): drop
|
|
145
170
|
* a hit whose summary is ≥ dedupSim similar to a live message — "dedupe on
|
|
146
171
|
* inline/read" so we never re-inject context already resident. */
|
|
@@ -253,6 +253,18 @@ export function loadConfig(): MegaConfig {
|
|
|
253
253
|
// Phase H: output-error catch — trip compaction on a truncated model output
|
|
254
254
|
// (S28 stopReason==='length'). Default ON; OFF byte-identical pre-H.
|
|
255
255
|
outputErrorCompact: envBool("MEGACOMPACT_OUTPUT_ERROR_COMPACT", true),
|
|
256
|
+
// v0.21.9 OUTPUT-HEADROOM GATE: fire compaction BEFORE the request
|
|
257
|
+
// overflows the model window (input + output reserve + margin >= window),
|
|
258
|
+
// not after. Percent-based: the reserve scales with the model's own window
|
|
259
|
+
// so the math holds at every window size (32k…5M). Default ON;
|
|
260
|
+
// OFF = byte-identical pre-v0.21.9 (2026-08-19 32k incident fix).
|
|
261
|
+
overflowHeadroom: envBool("MEGACOMPACT_OVERFLOW_HEADROOM", true),
|
|
262
|
+
// v0.21.9: fallback OUTPUT reserve as a FRACTION of the context window,
|
|
263
|
+
// used when the model's declared maxTokens is absent or implausible
|
|
264
|
+
// (0 / sentinel 1e9/1e38 / >= window). Clamped [0.1, 0.95]; default 0.30.
|
|
265
|
+
// When maxTokens IS plausible the declared value wins (vLLM reserves the
|
|
266
|
+
// full maxTokens) — this fraction is only the fallback.
|
|
267
|
+
outputReservePct: clamp(envFlag("MEGACOMPACT_OUTPUT_RESERVE_PCT", 0.3), 0.1, 0.95),
|
|
256
268
|
windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
|
|
257
269
|
recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
|
|
258
270
|
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
@@ -142,11 +142,14 @@ export interface DashboardSnapshot {
|
|
|
142
142
|
outputRate: number; // USD per output token (Model.cost)
|
|
143
143
|
};
|
|
144
144
|
/** v0.8.8 Perf dashboard: live diag counters (skip vs recompute vs replay)
|
|
145
|
-
* for the Perf tab's "TUI lag proxy" cards. Optional for back-compat.
|
|
145
|
+
* for the Perf tab's "TUI lag proxy" cards. Optional for back-compat.
|
|
146
|
+
* v0.21.9: headroomTrips added (output-headroom gate trips); readers that
|
|
147
|
+
* don't know it stay backward-compatible. */
|
|
146
148
|
diag?: {
|
|
147
149
|
ctxFastGate: number;
|
|
148
150
|
liveTrimFires: number;
|
|
149
151
|
liveTrimReplays: number;
|
|
152
|
+
headroomTrips?: number;
|
|
150
153
|
};
|
|
151
154
|
/** S38.8: error-retry state for dashboard "retries" tile.
|
|
152
155
|
* R7 (retry redesign): sessionRetryCount / sessionMax / poisonedCount are
|
|
@@ -18,6 +18,7 @@ import { autoCompactCheck } from "../../../src/compact.js";
|
|
|
18
18
|
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
19
19
|
import type { MegaConfig } from "../../mega-config.js";
|
|
20
20
|
import { isThrashBlockedFor } from "./thrashGuard.js";
|
|
21
|
+
import { resolveOutputReserve } from "./headroom.js";
|
|
21
22
|
|
|
22
23
|
/** Tail-injection closure shape produced by buildTailResult (tailResult.ts). */
|
|
23
24
|
export type TailResultFn = (
|
|
@@ -30,6 +31,14 @@ export type GateOutcome =
|
|
|
30
31
|
| {
|
|
31
32
|
kind: "proceed";
|
|
32
33
|
perModelThreshold: { safetyMarginPct: number; firePointPct: number };
|
|
34
|
+
/**
|
|
35
|
+
* v0.21.9: true when the proceed was forced by the output-headroom
|
|
36
|
+
* check (the request would overflow the model window before reaching
|
|
37
|
+
* the percent/token fire point). Consumed by thrashGuardBlocks so an
|
|
38
|
+
* overflow-bound fire is never refused by the thrash guard — an
|
|
39
|
+
* overflowed session is unrecoverable (2026-08-19 32k incident).
|
|
40
|
+
*/
|
|
41
|
+
headroomExceeded?: boolean;
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -55,9 +64,16 @@ export function thrashGuardBlocks(
|
|
|
55
64
|
runtime: MegaRuntime,
|
|
56
65
|
config: MegaConfig,
|
|
57
66
|
currentTokens: number | null | undefined,
|
|
67
|
+
headroomExceeded?: boolean,
|
|
58
68
|
): boolean {
|
|
59
69
|
if (!config.threeWayFailback) return false;
|
|
60
70
|
if (currentTokens == null) return false;
|
|
71
|
+
// v0.21.9: an overflow-bound fire (headroomExceeded) is EXEMPT from the
|
|
72
|
+
// thrash guard. The guard exists to stop wasted re-compaction when the
|
|
73
|
+
// window refuses to shrink; but an overflowed request is not "wasted work"
|
|
74
|
+
// — it is the model about to 400. Blocking that fire reproduces the
|
|
75
|
+
// 2026-08-19 32k deadlock (compact never → request > window → error loop).
|
|
76
|
+
if (headroomExceeded) return false;
|
|
61
77
|
return isThrashBlockedFor(runtime, currentTokens, runtime.currentStateDir);
|
|
62
78
|
}
|
|
63
79
|
|
|
@@ -113,6 +129,57 @@ export function evaluateGate(
|
|
|
113
129
|
return { kind: "proceed", perModelThreshold };
|
|
114
130
|
}
|
|
115
131
|
|
|
132
|
+
// v0.21.9 OUTPUT-HEADROOM GATE (the root-cause fix for the 32k truncation
|
|
133
|
+
// loop). The percent/token fire points above judge only INPUT utilization
|
|
134
|
+
// (tier% of the window), but a request's budget is
|
|
135
|
+
// input tokens + the model's output budget + safety margin.
|
|
136
|
+
// On a small-window model with a large maxTokens (the user's 32k/20k
|
|
137
|
+
// GLM-4.7), the request overflows at ~32% INPUT (21.4k + 20k > 32.768k) —
|
|
138
|
+
// long before any percent gate fires → provider 400 "request exceeds the
|
|
139
|
+
// available context size" every turn → the poisoned-error loop. Phase H only
|
|
140
|
+
// reacts to stopReason 'length' (mid-output truncation); a pre-output 400
|
|
141
|
+
// never arms it, so "compact never". This check fires the compaction
|
|
142
|
+
// BEFORE the overflow instead of after.
|
|
143
|
+
//
|
|
144
|
+
// PERCENT-BASED (LTS invariant — must work at every window size: 32k, 64k,
|
|
145
|
+
// 200k, 1M, 5M): the reserve is a FRACTION of the model's own window via
|
|
146
|
+
// resolveOutputReserve (plausible declared maxTokens wins, else
|
|
147
|
+
// clamp(MEGACOMPACT_OUTPUT_RESERVE_PCT, 10–95%) × window). Same math, any
|
|
148
|
+
// size. window <= 0 (unknown) ⇒ deferred (never guess a window), matching
|
|
149
|
+
// the effectiveThresholdImpl Phase-C invariant. Gated on
|
|
150
|
+
// config.overflowHeadroom (default ON; OFF = byte-identical pre-v0.21.9).
|
|
151
|
+
// Thrash-guard exemption: headroomExceeded rides along on the proceed so the
|
|
152
|
+
// handler's thrash consult never refuses an overflow-bound fire (see
|
|
153
|
+
// thrashGuardBlocks above) — an overflowed session is unrecoverable, so a
|
|
154
|
+
// wasted re-fire is always the better outcome (2026-08-19 incident).
|
|
155
|
+
if (
|
|
156
|
+
config.overflowHeadroom &&
|
|
157
|
+
runtime.lastCtxWindow > 0 &&
|
|
158
|
+
Number.isFinite(currentTokens) &&
|
|
159
|
+
currentTokens > 0
|
|
160
|
+
) {
|
|
161
|
+
const { reserveTokens, fallbackUsed } = resolveOutputReserve(
|
|
162
|
+
runtime.lastCtxWindow,
|
|
163
|
+
runtime.currentModel?.maxTokens ?? 0,
|
|
164
|
+
config.outputReservePct,
|
|
165
|
+
);
|
|
166
|
+
const headroomMargin = Math.ceil(
|
|
167
|
+
runtime.lastCtxWindow * (perModelThreshold.safetyMarginPct / 100),
|
|
168
|
+
);
|
|
169
|
+
if (currentTokens + reserveTokens + headroomMargin >= runtime.lastCtxWindow) {
|
|
170
|
+
runtime.diagCtxHeadroomTrip++;
|
|
171
|
+
runtime.logger.info("gate-headroom-trip", {
|
|
172
|
+
sessionId: runtime.rt.sessionId,
|
|
173
|
+
currentTokens,
|
|
174
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
175
|
+
reserveTokens,
|
|
176
|
+
fallbackUsed,
|
|
177
|
+
marginPct: perModelThreshold.safetyMarginPct,
|
|
178
|
+
});
|
|
179
|
+
return { kind: "proceed", perModelThreshold, headroomExceeded: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
116
183
|
// S29 FAST GATE: `custom` (absolute MEGACOMPACT_THRESHOLD_TOKENS,
|
|
117
184
|
// tierPct null) is an explicit opt-out of percent scaling — it keeps the
|
|
118
185
|
// token gate. When pct is unavailable (window unknown / a model that
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/headroom.ts — output-headroom reserve math + pair-safe tail cap.
|
|
3
|
+
*
|
|
4
|
+
* v0.21.9. Single source of truth for the output reserve used by BOTH the
|
|
5
|
+
* gate's pre-fire overflow check (gateCheck.ts) and the live-trim tail cap
|
|
6
|
+
* (liveTrim.ts + the D.2/D.3 replay paths). The pre-v0.21.9 code computed the
|
|
7
|
+
* reserve inline in liveTrim only and never in the gate — the two halves could
|
|
8
|
+
* drift, and the gate had no output awareness at all.
|
|
9
|
+
*
|
|
10
|
+
* PERCENT-BASED BY DESIGN (per the LTS invariant): every quantity is expressed
|
|
11
|
+
* as a fraction of the MODEL'S OWN context window, so the math is identical at
|
|
12
|
+
* any window size — 32k, 64k, 200k, 1M, 5M. The reserve is the model's
|
|
13
|
+
* declared max output tokens when plausible, else a clamped fraction of the
|
|
14
|
+
* window (MEGACOMPACT_OUTPUT_RESERVE_PCT, default 30%, clamped 10–95%).
|
|
15
|
+
*
|
|
16
|
+
* Pure functions, no runtime dependency — trivially unit-testable headlessly.
|
|
17
|
+
*/
|
|
18
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
|
+
import { estimateBlockTokens, estimateMessageTokens } from "../../../src/tokens.js";
|
|
20
|
+
import { messageContentText } from "./messageText.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The model's declared maxTokens is only trusted as the output budget when it
|
|
24
|
+
* is plausible. models.json carries sentinel junk for some entries (1e9,
|
|
25
|
+
* 1e38, "unlimited"), and some providers report 0/absent. A declared budget
|
|
26
|
+
* above this FRACTION of the window is implausible — fall back to the
|
|
27
|
+
* configured fraction so a 200k/1e9 model doesn't compute a negative budget
|
|
28
|
+
* and silently disable the cap (the pre-v0.21.9 bug). Percent-based: holds at
|
|
29
|
+
* every window size.
|
|
30
|
+
*
|
|
31
|
+
* WHY 0.95 AND NOT LOWER: vLLM-style backends reject a request when
|
|
32
|
+
* `input + max_tokens > context window` — they reserve the model's FULL
|
|
33
|
+
* declared maxTokens, not a fraction of it. The user's own GLM-4.7 entry is
|
|
34
|
+
* 32000/20000 (62.5%); a 0.6 cutoff rejected that REAL config as
|
|
35
|
+
* "implausible" and fell back to a 30% reserve (9600) while the backend
|
|
36
|
+
* reserved the full 20000 — the gate would keep firing late and the
|
|
37
|
+
* post-compact tail would still overflow (2026-08-19 incident, attempt #6).
|
|
38
|
+
* A declared budget is plausible up to just below the WHOLE window; anything
|
|
39
|
+
* at/above the window (or the 1e9/1e38 sentinels) is junk.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_OUTPUT_PLAUSIBLE_FRACTION = 0.95;
|
|
42
|
+
|
|
43
|
+
/** Bounds for the fallback reserve fraction (MEGACOMPACT_OUTPUT_RESERVE_PCT). */
|
|
44
|
+
export const OUTPUT_RESERVE_PCT_MIN = 0.1;
|
|
45
|
+
export const OUTPUT_RESERVE_PCT_MAX = 0.95;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the output reserve (tokens) for a model window.
|
|
49
|
+
*
|
|
50
|
+
* - window <= 0 (unknown) → { reserveTokens: 0, fallbackUsed: false }; every
|
|
51
|
+
* consumer is guarded on window > 0 and defers (never guesses a window).
|
|
52
|
+
* - maxTokens plausible (0 < maxTokens <= 95% of the window) → maxTokens —
|
|
53
|
+
* vLLM-style backends reserve the FULL declared maxTokens, so the reserve
|
|
54
|
+
* must equal it, not a fraction of it.
|
|
55
|
+
* - otherwise → clamp(outputReservePct, 0.1, 0.95) × window.
|
|
56
|
+
*
|
|
57
|
+
* `outputReservePct` is config.outputReservePct (already env-clamped at load,
|
|
58
|
+
* re-clamped here for defense against direct callers).
|
|
59
|
+
*/
|
|
60
|
+
export function resolveOutputReserve(
|
|
61
|
+
ctxWindow: number,
|
|
62
|
+
maxTokens: number,
|
|
63
|
+
outputReservePct: number,
|
|
64
|
+
): { reserveTokens: number; fallbackUsed: boolean } {
|
|
65
|
+
if (!Number.isFinite(ctxWindow) || ctxWindow <= 0) {
|
|
66
|
+
return { reserveTokens: 0, fallbackUsed: false };
|
|
67
|
+
}
|
|
68
|
+
const plausible =
|
|
69
|
+
Number.isFinite(maxTokens) &&
|
|
70
|
+
maxTokens > 0 &&
|
|
71
|
+
maxTokens <= ctxWindow * MAX_OUTPUT_PLAUSIBLE_FRACTION;
|
|
72
|
+
if (plausible) return { reserveTokens: Math.round(maxTokens), fallbackUsed: false };
|
|
73
|
+
const pct = Math.min(
|
|
74
|
+
OUTPUT_RESERVE_PCT_MAX,
|
|
75
|
+
Math.max(OUTPUT_RESERVE_PCT_MIN, Number.isFinite(outputReservePct) ? outputReservePct : 0.3),
|
|
76
|
+
);
|
|
77
|
+
return { reserveTokens: Math.ceil(ctxWindow * pct), fallbackUsed: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pair-safe front-drop for the live-trim tail cap. Drops OLDEST messages from
|
|
82
|
+
* the front of `recentRaw` until the remaining tail fits
|
|
83
|
+
* `ctxWindow − outputReserve − safetyMargin − summaryTokens`, then advances
|
|
84
|
+
* the start index past any leading toolResult messages so the preserved tail
|
|
85
|
+
* never begins on an orphaned toolResult (PREVENT-PI-002: a toolCall/toolResult
|
|
86
|
+
* pair must not be split). Never returns an empty tail — the final message is
|
|
87
|
+
* always kept so the agent can respond.
|
|
88
|
+
*
|
|
89
|
+
* v0.21.9 hardenings over the pre-v0.21.9 inline cap in liveTrim.ts:
|
|
90
|
+
* 1. BUDGET FLOOR — when the reserve exceeds the window (implausible maxTokens
|
|
91
|
+
* made budget <= 0) the old block silently skipped the cap entirely and an
|
|
92
|
+
* oversized tail sailed past the window. Now the reserve is clamped (via
|
|
93
|
+
* resolveOutputReserve) to a fraction of the window, so a floor budget
|
|
94
|
+
* always exists. If even ONE message exceeds the floor budget we keep only
|
|
95
|
+
* the final message — the agent's last turn is the one thing the model
|
|
96
|
+
* must always see.
|
|
97
|
+
* 2. TOOL-PAIR SAFETY — the old front-drop could land between a toolCall and
|
|
98
|
+
* its toolResult, splitting the pair.
|
|
99
|
+
*
|
|
100
|
+
* Pure: returns { recent, dropped } without touching the input array.
|
|
101
|
+
*/
|
|
102
|
+
export function applyTailCap(opts: {
|
|
103
|
+
recentRaw: readonly AgentMessage[];
|
|
104
|
+
summaryTokens: number;
|
|
105
|
+
ctxWindow: number;
|
|
106
|
+
maxOutputTokens: number;
|
|
107
|
+
outputReservePct: number;
|
|
108
|
+
safetyMarginPct: number;
|
|
109
|
+
/**
|
|
110
|
+
* Optional token-count override for the tail messages. When present, the
|
|
111
|
+
* token sum is counted from this array (index-aligned with `recentRaw`);
|
|
112
|
+
* otherwise each message's tokens are estimated from its text content.
|
|
113
|
+
*/
|
|
114
|
+
messageTokens?: readonly number[];
|
|
115
|
+
}): { recent: AgentMessage[]; dropped: number } {
|
|
116
|
+
const { recentRaw, summaryTokens, ctxWindow, outputReservePct } = opts;
|
|
117
|
+
if (ctxWindow <= 0 || recentRaw.length <= 1) {
|
|
118
|
+
return { recent: [...recentRaw], dropped: 0 };
|
|
119
|
+
}
|
|
120
|
+
const msgTokens =
|
|
121
|
+
opts.messageTokens && opts.messageTokens.length === recentRaw.length
|
|
122
|
+
? opts.messageTokens
|
|
123
|
+
: null;
|
|
124
|
+
const { reserveTokens } = resolveOutputReserve(
|
|
125
|
+
ctxWindow,
|
|
126
|
+
opts.maxOutputTokens,
|
|
127
|
+
outputReservePct,
|
|
128
|
+
);
|
|
129
|
+
const safetyMargin = Math.ceil(
|
|
130
|
+
ctxWindow * (Math.max(0, opts.safetyMarginPct) / 100),
|
|
131
|
+
);
|
|
132
|
+
// Budget floor: never negative. An implausible reserve (clamped above to
|
|
133
|
+
// <= 95% of the window) plus margin + summary can still exceed the window
|
|
134
|
+
// on tiny summaries-free edges; the floor keeps the cap alive with a small
|
|
135
|
+
// positive budget instead of disabling it (pre-v0.21.9 behavior).
|
|
136
|
+
const budget = Math.max(
|
|
137
|
+
1,
|
|
138
|
+
ctxWindow - reserveTokens - safetyMargin - Math.max(0, summaryTokens),
|
|
139
|
+
);
|
|
140
|
+
let start = 0;
|
|
141
|
+
let tailTokens = 0;
|
|
142
|
+
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
143
|
+
tailTokens +=
|
|
144
|
+
msgTokens != null
|
|
145
|
+
? Math.max(0, msgTokens[i])
|
|
146
|
+
: estimateMessageTokens({ text: messageContentText(recentRaw[i]) });
|
|
147
|
+
if (tailTokens > budget) {
|
|
148
|
+
// Keep from i+1 onward; never drop below the FINAL message.
|
|
149
|
+
start = Math.min(i + 1, recentRaw.length - 1);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// PREVENT-PI-002: never begin the preserved tail on an orphaned toolResult —
|
|
154
|
+
// its toolCall was dropped by the front-cut above. Advance past consecutive
|
|
155
|
+
// toolResults; the pair stays intact or drops whole.
|
|
156
|
+
while (
|
|
157
|
+
start < recentRaw.length - 1 &&
|
|
158
|
+
(recentRaw[start] as { role?: string }).role === "toolResult"
|
|
159
|
+
) {
|
|
160
|
+
start++;
|
|
161
|
+
}
|
|
162
|
+
return { recent: recentRaw.slice(start), dropped: start };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* v0.21.9: re-cap a REPLAYED trim tail (D.2 in context-handler.ts, D.3 in
|
|
167
|
+
* pipelineRun.ts). The replay paths return the cached trim view verbatim —
|
|
168
|
+
* which bypasses the fire-time tail cap. A model switch mid-epoch can shrink
|
|
169
|
+
* the window, leaving a replayed tail that fit the OLD window overflowing the
|
|
170
|
+
* NEW one. Re-runs applyTailCap against the CURRENT window with the margin
|
|
171
|
+
* stored at fire time (trimCache.safetyMarginPct), so the replayed view never
|
|
172
|
+
* exceeds what the gate would allow. Pure — no runtime dependency.
|
|
173
|
+
*/
|
|
174
|
+
export function recapReplayedTail(opts: {
|
|
175
|
+
recentRaw: readonly AgentMessage[];
|
|
176
|
+
summaryAgentMsg: AgentMessage;
|
|
177
|
+
ctxWindow: number;
|
|
178
|
+
maxOutputTokens: number;
|
|
179
|
+
outputReservePct: number;
|
|
180
|
+
safetyMarginPct: number;
|
|
181
|
+
}): { recent: AgentMessage[]; dropped: number } {
|
|
182
|
+
return applyTailCap({
|
|
183
|
+
recentRaw: opts.recentRaw,
|
|
184
|
+
summaryTokens: estimateBlockTokens(messageContentText(opts.summaryAgentMsg)),
|
|
185
|
+
ctxWindow: opts.ctxWindow,
|
|
186
|
+
maxOutputTokens: opts.maxOutputTokens,
|
|
187
|
+
outputReservePct: opts.outputReservePct,
|
|
188
|
+
safetyMarginPct: opts.safetyMarginPct,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -13,12 +13,9 @@
|
|
|
13
13
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
15
15
|
import type { EngineMessage } from "../../../src/types.js";
|
|
16
|
-
import {
|
|
17
|
-
estimateBlockTokens,
|
|
18
|
-
estimateMessageTokens,
|
|
19
|
-
} from "../../../src/tokens.js";
|
|
16
|
+
import { estimateBlockTokens } from "../../../src/tokens.js";
|
|
20
17
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../../mega-trim.js";
|
|
21
|
-
import {
|
|
18
|
+
import { applyTailCap } from "./headroom.js";
|
|
22
19
|
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
23
20
|
import type { MegaConfig } from "../../mega-config.js";
|
|
24
21
|
import type { TailResultFn } from "./gateCheck.js";
|
|
@@ -136,59 +133,33 @@ export function buildLiveTrimView(
|
|
|
136
133
|
// but has NO token cap, so a 2-message tail of two 80K bash outputs sails
|
|
137
134
|
// right past the window.
|
|
138
135
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
136
|
+
// v0.21.9: the reserve + front-drop now lives in headroom.ts (single
|
|
137
|
+
// source shared with the gate's pre-fire headroom check and the D.2/D.3
|
|
138
|
+
// replay paths): (a) percent-based reserve — plausible declared maxTokens
|
|
139
|
+
// wins, else clamp(MEGACOMPACT_OUTPUT_RESERVE_PCT, 10–95%) × window — so
|
|
140
|
+
// the math is identical at any window size and a sentinel maxTokens
|
|
141
|
+
// (1e9/1e38) can no longer drive the budget negative and silently
|
|
142
|
+
// disable the cap; (b) budget floor (max(1, …)) so the cap stays active
|
|
143
|
+
// on every window; (c) pair-safe front-drop — the preserved tail never
|
|
144
|
+
// begins on an orphaned toolResult (PREVENT-PI-002).
|
|
145
145
|
const ctxWindow = runtime.lastCtxWindow;
|
|
146
146
|
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
147
147
|
const modelThreshold = perModelThreshold;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
ctxWindow
|
|
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
|
-
}
|
|
148
|
+
const { recent, dropped } = applyTailCap({
|
|
149
|
+
recentRaw,
|
|
150
|
+
summaryTokens: estimateBlockTokens(summaryMsg.text),
|
|
151
|
+
ctxWindow,
|
|
152
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
153
|
+
outputReservePct: config.outputReservePct,
|
|
154
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
155
|
+
});
|
|
156
|
+
if (dropped > 0) {
|
|
157
|
+
runtime.logger.warn("live-trim-tail-cap", {
|
|
158
|
+
sessionId: runtime.rt.sessionId,
|
|
159
|
+
dropped,
|
|
160
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
161
|
+
ctxWindow,
|
|
162
|
+
});
|
|
192
163
|
}
|
|
193
164
|
|
|
194
165
|
// v0.8.6: cache the trim view so subsequent gated calls in this epoch
|
|
@@ -214,6 +185,11 @@ export function buildLiveTrimView(
|
|
|
214
185
|
summaryAgentMsg,
|
|
215
186
|
ctxPct: pct ?? null,
|
|
216
187
|
ctxTokens: currentTokens,
|
|
188
|
+
// v0.21.9: the D.2/D.3 replay paths re-cap the replayed tail against
|
|
189
|
+
// the CURRENT window (a model switch can change it mid-epoch). The
|
|
190
|
+
// margin % used at fire time is stored alongside so the replay uses
|
|
191
|
+
// the same reserve math as the fire that built the view.
|
|
192
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
217
193
|
};
|
|
218
194
|
runtime.snapshot(ctx);
|
|
219
195
|
// DIAG (team-run relief): confirm the live trim actually fires + how big
|
|
@@ -21,6 +21,7 @@ import type { MegaConfig } from "../../mega-config.js";
|
|
|
21
21
|
import type { TailResultFn } from "./gateCheck.js";
|
|
22
22
|
import { recordCompactLatency } from "../../mega-runtime/vc-observer.js";
|
|
23
23
|
import { decideLivePath } from "../../mega-runtime/vector-cortex-live.js";
|
|
24
|
+
import { recapReplayedTail } from "./headroom.js";
|
|
24
25
|
import { defaultClock, type RolloutEvidence } from "../../../src/vector-cortex/rollout/gate.js";
|
|
25
26
|
import { VC5C_ENABLED } from "../../../src/config/vector-cortex.js";
|
|
26
27
|
|
|
@@ -120,7 +121,19 @@ export function invokePipeline(
|
|
|
120
121
|
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
121
122
|
runtime.trimCache.cut <= opts.messages.length
|
|
122
123
|
) {
|
|
123
|
-
const
|
|
124
|
+
const recentRaw = 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.
|
|
125
|
+
// v0.21.9: RE-CAP the replayed tail against the CURRENT window —
|
|
126
|
+
// the D.3 skip-replay bypasses the fire-time tail cap exactly like
|
|
127
|
+
// D.2; a model switch mid-epoch can shrink the window below what
|
|
128
|
+
// the cached view was built for. No-op when the tail already fits.
|
|
129
|
+
const { recent } = recapReplayedTail({
|
|
130
|
+
recentRaw,
|
|
131
|
+
summaryAgentMsg: runtime.trimCache.summaryAgentMsg,
|
|
132
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
133
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
134
|
+
outputReservePct: config.outputReservePct,
|
|
135
|
+
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
136
|
+
});
|
|
124
137
|
runtime.diagLiveTrimFires++;
|
|
125
138
|
runtime.diagLiveTrimReplays++;
|
|
126
139
|
runtime.snapshot(ctx);
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./context-handler/thrashGuard.js";
|
|
35
35
|
import { invokePipeline } from "./context-handler/pipelineRun.js";
|
|
36
36
|
import { buildLiveTrimView } from "./context-handler/liveTrim.js";
|
|
37
|
+
import { recapReplayedTail } from "./context-handler/headroom.js";
|
|
37
38
|
|
|
38
39
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
39
40
|
export function registerContextHandler(
|
|
@@ -173,7 +174,23 @@ export function registerContextHandler(
|
|
|
173
174
|
: currentTokens - (runtime.trimCache.ctxTokens ?? 0) >=
|
|
174
175
|
runtime.effectiveThreshold * 0.5;
|
|
175
176
|
if (!grewEnough) {
|
|
176
|
-
const
|
|
177
|
+
const recentRaw = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized once by computeLiveTrimCut (src/boundary.ts) and replayed verbatim; the transcript only grows within an epoch (cache is cleared on durable truncation), so the preserved run still starts on a toolPair-safe index.
|
|
178
|
+
// v0.21.9: RE-CAP the replayed tail against the CURRENT window.
|
|
179
|
+
// Replay returns the cached view verbatim, which bypasses the
|
|
180
|
+
// fire-time tail cap — a model switch mid-epoch can shrink the
|
|
181
|
+
// window and leave a replayed tail that fit the OLD window
|
|
182
|
+
// overflowing the NEW one. Same reserve math as the fire
|
|
183
|
+
// (margin % stored in the cache at fire time); no-op when the
|
|
184
|
+
// tail already fits. Pair-safe (applyTailCap advances past any
|
|
185
|
+
// leading toolResult its front-drop exposes).
|
|
186
|
+
const { recent } = recapReplayedTail({
|
|
187
|
+
recentRaw,
|
|
188
|
+
summaryAgentMsg: runtime.trimCache.summaryAgentMsg,
|
|
189
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
190
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
191
|
+
outputReservePct: config.outputReservePct,
|
|
192
|
+
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
193
|
+
});
|
|
177
194
|
runtime.diagLiveTrimFires++; // trim view returned this call (replay counts as a fire)
|
|
178
195
|
runtime.diagLiveTrimReplays++;
|
|
179
196
|
runtime.snapshot(ctx);
|
|
@@ -194,7 +211,7 @@ export function registerContextHandler(
|
|
|
194
211
|
// invokePipeline (the real fire point), so it covers the percent + token
|
|
195
212
|
// gate paths alike. Umbrella OFF ⇒ never blocks (byte-identical). Returns
|
|
196
213
|
// the tailed view so a staged recall block still rides along.
|
|
197
|
-
if (thrashGuardBlocks(runtime, config, currentTokens)) {
|
|
214
|
+
if (thrashGuardBlocks(runtime, config, currentTokens, gate.headroomExceeded)) {
|
|
198
215
|
runtime.diagCtxFastGate++;
|
|
199
216
|
runtime.snapshot(ctx);
|
|
200
217
|
return tailResult() ?? undefined;
|
|
@@ -49,6 +49,8 @@ export interface SnapshotBuildContext {
|
|
|
49
49
|
readonly diagCtxFastGate: number;
|
|
50
50
|
readonly diagLiveTrimFires: number;
|
|
51
51
|
readonly diagLiveTrimReplays: number;
|
|
52
|
+
/** v0.21.9: output-headroom gate trips (pre-overflow compaction fires). */
|
|
53
|
+
readonly diagCtxHeadroomTrip: number;
|
|
52
54
|
readonly errorRetryCount: number;
|
|
53
55
|
readonly consecutiveErrors: number;
|
|
54
56
|
readonly ERROR_RETRY_MAX_CONSECUTIVE: number;
|
|
@@ -190,6 +192,7 @@ export function buildDashboardSnapshot(ctx: SnapshotBuildContext): DashboardSnap
|
|
|
190
192
|
ctxFastGate: ctx.diagCtxFastGate,
|
|
191
193
|
liveTrimFires: ctx.diagLiveTrimFires,
|
|
192
194
|
liveTrimReplays: ctx.diagLiveTrimReplays,
|
|
195
|
+
headroomTrips: ctx.diagCtxHeadroomTrip,
|
|
193
196
|
},
|
|
194
197
|
retries: {
|
|
195
198
|
errorRetryCount: ctx.errorRetryCount,
|
|
@@ -39,6 +39,7 @@ export class RuntimeInstrumentation {
|
|
|
39
39
|
diagCtxCutNull = 0; // computeLiveTrimCut returned null (anchor/boundary)
|
|
40
40
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
41
41
|
diagCtxOutputErrorTrip = 0; // Phase H: output-error catch tripped a forced compaction
|
|
42
|
+
diagCtxHeadroomTrip = 0; // v0.21.9: output-headroom gate tripped a pre-overflow compaction
|
|
42
43
|
|
|
43
44
|
// Context health instrumentation (v0.12): rolling ring buffers for
|
|
44
45
|
// drift detection + cache poison Layer 1 hash baseline.
|
|
@@ -79,6 +80,9 @@ export class RuntimeInstrumentation {
|
|
|
79
80
|
summaryAgentMsg: AgentMessage;
|
|
80
81
|
ctxPct: number | null;
|
|
81
82
|
ctxTokens: number | null;
|
|
83
|
+
/** v0.21.9: safety margin % recorded at fire time so the D.2/D.3 replay
|
|
84
|
+
* paths can re-cap the replayed tail with the same reserve math. */
|
|
85
|
+
safetyMarginPct: number;
|
|
82
86
|
} | null = null;
|
|
83
87
|
debounceUntil = 0;
|
|
84
88
|
// S16: debounce for the agent_end resume nudge (avoid busy-loops).
|
|
@@ -84,6 +84,7 @@ export interface RuntimeSnapshotContext extends RuntimeHelpersContext {
|
|
|
84
84
|
diagCtxFastGate: number;
|
|
85
85
|
diagLiveTrimFires: number;
|
|
86
86
|
diagLiveTrimReplays: number;
|
|
87
|
+
diagCtxHeadroomTrip: number;
|
|
87
88
|
|
|
88
89
|
// ── public methods the orchestration calls ──
|
|
89
90
|
bindRepo(cwd: string | undefined): string;
|
|
@@ -177,6 +178,7 @@ export function snapshotImpl(
|
|
|
177
178
|
diagCtxFastGate: self.diagCtxFastGate,
|
|
178
179
|
diagLiveTrimFires: self.diagLiveTrimFires,
|
|
179
180
|
diagLiveTrimReplays: self.diagLiveTrimReplays,
|
|
181
|
+
diagCtxHeadroomTrip: self.diagCtxHeadroomTrip,
|
|
180
182
|
errorRetryCount: self.rt.errorRetryCount,
|
|
181
183
|
consecutiveErrors: self.rt.consecutiveErrors,
|
|
182
184
|
ERROR_RETRY_MAX_CONSECUTIVE: self.config.maxConsecutiveErrors,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.9",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|