pi-mega-compact 0.21.11 → 0.21.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-compaction.js +2 -0
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-events/context-handler/headroom.js +9 -1
- package/dist/extensions/mega-events/context-handler/liveTrim.js +33 -1
- package/dist/extensions/mega-events/context-handler/pipelineRun.js +1 -0
- package/dist/extensions/mega-events/context-handler/wireTruth.js +128 -0
- package/dist/extensions/mega-events/context-handler/wireTruthApply.js +121 -0
- package/dist/extensions/mega-events/context-handler.js +68 -6
- package/dist/extensions/mega-runtime/runtime-instrumentation.js +2 -0
- package/extensions/dashboard-server/routes-rag-settings-compaction.ts +14 -0
- package/extensions/mega-config-types.ts +14 -0
- package/extensions/mega-config.ts +10 -0
- package/extensions/mega-events/context-handler/headroom.ts +20 -1
- package/extensions/mega-events/context-handler/liveTrim.ts +37 -0
- package/extensions/mega-events/context-handler/pipelineRun.ts +2 -0
- package/extensions/mega-events/context-handler/wireTruth.ts +132 -0
- package/extensions/mega-events/context-handler/wireTruthApply.ts +168 -0
- package/extensions/mega-events/context-handler.ts +76 -6
- package/extensions/mega-runtime/runtime-instrumentation.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A local-first context compressor for the [pi coding agent](https://github.com/ea
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Auto-compaction** — the store watches context pressure and compacts quietly in the background. You'll notice when a long session just stays long while the token gauge rests comfortably far from the ceiling.
|
|
10
|
+
- **Small-context models are the point** — models with 32k windows (GLM-4.7 etc.) are a first-class case, not an afterthought. The gate accounts for the provider's full declared output reserve, and the live-trim budgets the tail so `input + reserve + margin <= window` — no truncation loops at the overflow edge. Token accounting counts everything the provider actually receives (thinking blocks, tool-call arguments included), not just visible text.
|
|
10
11
|
- **Two-layer compaction** — every LLM call sees a live trim of the context window, and every trim is checkpointed to SQLite so a crash or a `/clear` never loses the work.
|
|
11
12
|
- **Semantic dedup, three layers deep** — exact hash (L0) -> MinHash/LSH (L1) -> cosine over trigram embeddings (L2). The dedup audit log records per-tier decisions with similarity scores for tuning.
|
|
12
13
|
- **RAPTOR memory hierarchy** — decisions you made an hour ago don't scroll off; they get packed up as hierarchical checkpoints and re-inlined the moment your next session asks for them. Multi-level retrieval (leaves + summary clusters) is on by default. Since v0.11.10, RAPTOR tree updates are incremental (no full rebuild) — enabled by default.
|
|
@@ -94,6 +95,11 @@ Set env vars before starting pi. Defaults are in `src/config/dedup.ts`.
|
|
|
94
95
|
| `MEGACOMPACT_NEW_UI` | `true` | Use the new Tailwind/shadcn dashboard shell |
|
|
95
96
|
| `MEGACOMPACT_COST_API_ENABLED` | `false` | Opt-in: fetch model pricing from an external API (PREVENT-PI-004 applies to defaults; opt-in features are exempt). Enriches dashboard cost data for models not in the local pricing table |
|
|
96
97
|
| `MEGACOMPACT_COST_API_URL` | _(unset)_ | OpenRouter-compatible model pricing endpoint (e.g. `https://openrouter.ai/api/v1/models`). Only contacted when `MEGACOMPACT_COST_API_ENABLED=true` |
|
|
98
|
+
| `MEGACOMPACT_OVERFLOW_HEADROOM` | `true` | Fire compaction before `input + output reserve + margin` exceeds the window (prevents provider 400s on small-context models) |
|
|
99
|
+
| `MEGACOMPACT_OUTPUT_RESERVE_PCT` | `0.30` | Fallback output reserve as a fraction of the window when the model's declared maxTokens is missing or implausible |
|
|
100
|
+
| `MEGACOMPACT_OUTPUT_ERROR_COMPACT` | `true` | One-shot force-compact when a response truncates mid-output (`stopReason: length`) |
|
|
101
|
+
| `MEGACOMPACT_WIRE_OVERHEAD` | `true` | Add the provider's invisible request overhead H (system prompt + tool definitions + extension systemPrompt prepends — never in the stored transcript) back into the token estimate for the headroom gate and tail cap; H is a per-model EMA of observed wire samples, else `MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT` × window. Closes the small-context-model 400 loop (attempt #9). OFF = byte-identical v0.21.11 |
|
|
102
|
+
| `MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT` | `0.15` | Fraction of the context window used as the overhead H when no wire sample has been observed yet for the model (clamped 0–0.85). Percent-based: identical math at every window size |
|
|
97
103
|
|
|
98
104
|
Full config reference: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md)
|
|
99
105
|
|
|
@@ -116,7 +122,7 @@ Detailed architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
|
|
116
122
|
|
|
117
123
|
```bash
|
|
118
124
|
npm run build # TypeScript compile
|
|
119
|
-
npm test # Build +
|
|
125
|
+
npm test # Build + 4400+ tests
|
|
120
126
|
npm run lint # Type check + guardrails scan
|
|
121
127
|
```
|
|
122
128
|
|
|
@@ -38,5 +38,7 @@ export const COMPACTION_SETTINGS = {
|
|
|
38
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
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
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
|
+
boolDirect("MEGACOMPACT_WIRE_OVERHEAD", "Invisible-Overhead Calibration", "Add the provider's fixed request overhead H (system prompt + tool definitions + extension systemPrompt prepends — everything pi adds at request time that NEVER appears in the stored transcript) back into the token estimate for the headroom gate + tail cap. H is a per-model EMA of observed wire samples, else the Wire-Overhead Default fraction of the window. Closes the 32k overflow loop (attempt #9). OFF = byte-identical v0.21.11.", true),
|
|
42
|
+
num("MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT", "Wire-Overhead Default %", "Fraction of the context window used as the overhead H when no wire sample has been observed yet for the model. Once a sample lands, the per-model EMA wins. Clamped 0–0.85, default 0.15 (15%). Percent-based: identical math at every window size.", 0.15, 0, 0.85),
|
|
41
43
|
],
|
|
42
44
|
};
|
|
@@ -259,6 +259,16 @@ export function loadConfig() {
|
|
|
259
259
|
contextHealthOutputQuality: envBool("MEGACOMPACT_CONTEXT_HEALTH_OUTPUT_QUALITY", true),
|
|
260
260
|
contextHealthCachePoison: envBool("MEGACOMPACT_CONTEXT_HEALTH_CACHE_POISON", true),
|
|
261
261
|
contextHealthMitigate: envBool("MEGACOMPACT_CONTEXT_HEALTH_MITIGATE", false),
|
|
262
|
+
// v0.21.12: invisible-overhead calibration — add the provider's fixed
|
|
263
|
+
// request overhead H (system+tools+extension prepends, never in the
|
|
264
|
+
// transcript) back into the token estimate for the headroom gate / tail
|
|
265
|
+
// cap. H is an EMA of observed wire samples per model, else
|
|
266
|
+
// wireOverheadDefaultPct × window. Default ON; OFF = byte-identical
|
|
267
|
+
// v0.21.11 (every H term is 0). Closes attempt #9 of the 32k overflow loop.
|
|
268
|
+
wireOverhead: envBool("MEGACOMPACT_WIRE_OVERHEAD", true),
|
|
269
|
+
// v0.21.12: fallback H as a fraction of the window when no EMA sample
|
|
270
|
+
// exists yet. Clamped [0, 0.85]; default 0.15. Percent-based.
|
|
271
|
+
wireOverheadDefaultPct: clamp(envFlag("MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT", 0.15), 0, 0.85),
|
|
262
272
|
// D.1: env-overridable recompact delta (minimum context growth % before
|
|
263
273
|
// re-compacting instead of replaying the cached live trim). Default 50.
|
|
264
274
|
recompactPctDelta: envFlag("MEGACOMPACT_RECOMPACT_PCT_DELTA", 50),
|
|
@@ -139,7 +139,14 @@ export function applyTailCap(opts) {
|
|
|
139
139
|
// <= 95% of the window) plus margin + summary can still exceed the window
|
|
140
140
|
// on tiny summaries-free edges; the floor keeps the cap alive with a small
|
|
141
141
|
// positive budget instead of disabling it (pre-v0.21.9 behavior).
|
|
142
|
-
|
|
142
|
+
// v0.21.12: also subtract the invisible overhead H so the cap bounds the
|
|
143
|
+
// ACTUAL wire prompt (messages + H), breaking the 400 loop when the estimate
|
|
144
|
+
// undercounts by the system-prompt/tool-definition overhead.
|
|
145
|
+
const budget = Math.max(1, ctxWindow -
|
|
146
|
+
reserveTokens -
|
|
147
|
+
safetyMargin -
|
|
148
|
+
Math.max(0, summaryTokens) -
|
|
149
|
+
Math.max(0, opts.overheadTokens ?? 0));
|
|
143
150
|
let start = 0;
|
|
144
151
|
let tailTokens = 0;
|
|
145
152
|
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
@@ -179,5 +186,6 @@ export function recapReplayedTail(opts) {
|
|
|
179
186
|
maxOutputTokens: opts.maxOutputTokens,
|
|
180
187
|
outputReservePct: opts.outputReservePct,
|
|
181
188
|
safetyMarginPct: opts.safetyMarginPct,
|
|
189
|
+
overheadTokens: opts.overheadTokens ?? 0,
|
|
182
190
|
});
|
|
183
191
|
}
|
|
@@ -6,7 +6,7 @@ import { applyTailCap } from "./headroom.js";
|
|
|
6
6
|
* call. Returns the tailed view, or undefined when no trim is safe this call.
|
|
7
7
|
*/
|
|
8
8
|
export function buildLiveTrimView(runtime, config, ctx, opts) {
|
|
9
|
-
const { messages, view, pct, currentTokens, usageTokens, pressure, ran, perModelThreshold, tailResult, } = opts;
|
|
9
|
+
const { messages, view, pct, currentTokens, usageTokens, pressure, ran, perModelThreshold, tailResult, overheadTokens = 0, } = opts;
|
|
10
10
|
// S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
|
|
11
11
|
// Non-destructive: pi keeps the real transcript; only this LLM call sees the
|
|
12
12
|
// trimmed window. We compute the cut on the engine view (pure, tested) then
|
|
@@ -52,6 +52,37 @@ export function buildLiveTrimView(runtime, config, ctx, opts) {
|
|
|
52
52
|
anchorUserMessages,
|
|
53
53
|
criticalOver: (pct ?? 0) >= 90,
|
|
54
54
|
});
|
|
55
|
+
// v0.21.12: CAP THE SKIP PATH. When computeLiveTrimCut returns null
|
|
56
|
+
// (anchor floor blocked cutting a fat recent tool pair, or the
|
|
57
|
+
// criticalOver hatch stayed closed because estimated pressure ≈80%),
|
|
58
|
+
// the pre-v0.21.12 code shipped the RAW untrimmed view — which, with
|
|
59
|
+
// the invisible overhead H uncounted, overflowed the window → 400
|
|
60
|
+
// again, forever (the entire v0.21.11 blind spot). This is the
|
|
61
|
+
// invariant "the trim path never ships a view the budget wouldn't
|
|
62
|
+
// allow": even when we cannot summarize, we still front-drop
|
|
63
|
+
// OLDEST messages until the RAW tail + overhead fits the budget, so
|
|
64
|
+
// the model is never fed a prompt that exceeds its window. Flag OFF
|
|
65
|
+
// ⇒ byte-identical to v0.21.11 (return raw, no cap).
|
|
66
|
+
if (config.wireOverhead && runtime.lastCtxWindow > 0) {
|
|
67
|
+
const { recent, dropped } = applyTailCap({
|
|
68
|
+
recentRaw: messages,
|
|
69
|
+
summaryTokens: 0,
|
|
70
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
71
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
72
|
+
outputReservePct: config.outputReservePct,
|
|
73
|
+
safetyMarginPct: perModelThreshold.safetyMarginPct,
|
|
74
|
+
overheadTokens,
|
|
75
|
+
});
|
|
76
|
+
if (dropped > 0) {
|
|
77
|
+
runtime.diagCtxSkipCapped++;
|
|
78
|
+
runtime.logger.info("skip_cap_applied", {
|
|
79
|
+
sessionId: runtime.rt.sessionId,
|
|
80
|
+
dropped,
|
|
81
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
82
|
+
});
|
|
83
|
+
return tailResult(recent) ?? { messages: recent };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
55
86
|
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
56
87
|
}
|
|
57
88
|
const summaryMsg = liveTrimSummaryMessage({
|
|
@@ -98,6 +129,7 @@ export function buildLiveTrimView(runtime, config, ctx, opts) {
|
|
|
98
129
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
99
130
|
outputReservePct: config.outputReservePct,
|
|
100
131
|
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
132
|
+
overheadTokens,
|
|
101
133
|
});
|
|
102
134
|
if (dropped > 0) {
|
|
103
135
|
runtime.logger.warn("live-trim-tail-cap", {
|
|
@@ -85,6 +85,7 @@ export function invokePipeline(pi, runtime, config, ctx, opts) {
|
|
|
85
85
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
86
86
|
outputReservePct: config.outputReservePct,
|
|
87
87
|
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
88
|
+
overheadTokens: opts.overheadTokens ?? 0,
|
|
88
89
|
});
|
|
89
90
|
runtime.diagLiveTrimFires++;
|
|
90
91
|
runtime.diagLiveTrimReplays++;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/wireTruth.ts — invisible-overhead calibration + wire-truth parse.
|
|
3
|
+
*
|
|
4
|
+
* Attempt #9 on the small-context-model overflow loop (2026-08-19 incident,
|
|
5
|
+
* 8 prior attempts). ROOT CAUSE: pi adds a FIXED OVERHEAD H at request time
|
|
6
|
+
* (system prompt + tool definitions + extension systemPrompt prepends) that
|
|
7
|
+
* NEVER appears in the stored transcript. Neither pi's estimateContextTokens nor
|
|
8
|
+
* our estimateSessionTokens/applyTailCap count H. So when the provider 400s with
|
|
9
|
+
* "request (39048 tokens) exceeds the available context size (32768 tokens)" we
|
|
10
|
+
* have been estimating ~18–26k and judging headroom against that — undercounting
|
|
11
|
+
* by ~50%, so the gate never trips correctly and a RAW uncapped view ships → 400
|
|
12
|
+
* again, forever (pi's one-shot overflow recovery only resets on user
|
|
13
|
+
* message_start, so auto-retries burn it permanently).
|
|
14
|
+
*
|
|
15
|
+
* Two mechanisms here:
|
|
16
|
+
* - parseWireTruth: regex the provider's 400 text into ground-truth
|
|
17
|
+
* request/available token counts. Pure + unit-tested.
|
|
18
|
+
* - Per-model overhead EMA: calibrate H from observed samples so the gate and
|
|
19
|
+
* tail cap can ADD it back. Persisted in the SQLite meta table (copy the
|
|
20
|
+
* thrashGuard.ts pattern) so it survives restarts and travels with the repo.
|
|
21
|
+
*
|
|
22
|
+
* Non-fatal EVERYWHERE: every store read/write is best-effort, swallowed on
|
|
23
|
+
* failure. Structured JSON logging only (logger + emit dual-sink, mirroring
|
|
24
|
+
* armThrashGuard's 3WF-5 pattern). No console.*, no network, no mocks.
|
|
25
|
+
*/
|
|
26
|
+
import { getMetaNumber, setMetaNumber } from "../../../src/store/sqlite.js";
|
|
27
|
+
/** EMA smoothing factor for the overhead calibration (fixed, no count needed). */
|
|
28
|
+
export const OVERHEAD_EMA_ALPHA = 0.4;
|
|
29
|
+
/** Safety clamp: an overhead sample may never exceed this fraction of the window. */
|
|
30
|
+
export const OVERHEAD_CLAMP_FRACTION = 0.85;
|
|
31
|
+
/** Meta key prefix; the model id is appended (meta stores integers only). */
|
|
32
|
+
export const OVERHEAD_META_PREFIX = "wire.overhead_ema.";
|
|
33
|
+
/**
|
|
34
|
+
* Parse the provider's overflow error text into ground-truth token counts.
|
|
35
|
+
*
|
|
36
|
+
* Matches strings like:
|
|
37
|
+
* "request (39048 tokens) exceeds the available context size (32768 tokens)"
|
|
38
|
+
* "request (39,048 tokens) exceeds the available context size (32,768 tokens)"
|
|
39
|
+
*
|
|
40
|
+
* Pure + side-effect free — trivially unit-testable. Returns null when the text
|
|
41
|
+
* does not match the expected shape (e.g. an unrelated error message).
|
|
42
|
+
*/
|
|
43
|
+
export function parseWireTruth(text) {
|
|
44
|
+
if (typeof text !== "string" || text.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const m = text.match(/request\s*\((\d[\d,]*)\s*tokens\)\s*exceeds the available context size\s*\((\d[\d,]*)\s*tokens\)/i);
|
|
47
|
+
if (!m)
|
|
48
|
+
return null;
|
|
49
|
+
const requestTokens = Number(m[1]?.replace(/,/g, ""));
|
|
50
|
+
const availableTokens = Number(m[2]?.replace(/,/g, ""));
|
|
51
|
+
if (!Number.isFinite(requestTokens) || !Number.isFinite(availableTokens))
|
|
52
|
+
return null;
|
|
53
|
+
if (requestTokens <= 0 || availableTokens <= 0)
|
|
54
|
+
return null;
|
|
55
|
+
return { requestTokens, availableTokens };
|
|
56
|
+
}
|
|
57
|
+
/** Build the meta key for a model's overhead EMA (stored ×100, integer). */
|
|
58
|
+
function overheadKey(modelId) {
|
|
59
|
+
return OVERHEAD_META_PREFIX + modelId;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the calibrated overhead (tokens) for a model. Returns 0 when absent or
|
|
63
|
+
* unreadable — non-fatal everywhere. When the context window is known, clamps the
|
|
64
|
+
* value into [0, 0.85 × ctxWindow] as a safety against a runaway EMA.
|
|
65
|
+
*/
|
|
66
|
+
export function readWireOverhead(modelId, stateDir, ctxWindow = 0) {
|
|
67
|
+
if (!modelId)
|
|
68
|
+
return 0;
|
|
69
|
+
try {
|
|
70
|
+
const stored = getMetaNumber(overheadKey(modelId), stateDir);
|
|
71
|
+
if (!Number.isFinite(stored) || stored <= 0)
|
|
72
|
+
return 0;
|
|
73
|
+
const frac = stored / 100;
|
|
74
|
+
let overhead = frac * (ctxWindow > 0 ? ctxWindow : 1);
|
|
75
|
+
if (ctxWindow > 0) {
|
|
76
|
+
overhead = Math.min(overhead, ctxWindow * OVERHEAD_CLAMP_FRACTION);
|
|
77
|
+
}
|
|
78
|
+
return Math.max(0, overhead);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return 0; // non-fatal: never fail the agent loop on a store read
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fold a new overhead sample into the per-model EMA and persist it. Returns the
|
|
86
|
+
* new EMA in tokens (0 when the sample is invalid). First sample initializes the
|
|
87
|
+
* EMA directly (no warm-up). Best-effort: never throws; a store failure is
|
|
88
|
+
* swallowed and the in-memory EMA is still returned.
|
|
89
|
+
*
|
|
90
|
+
* Clamped into [0, 0.85 × ctxWindow] when ctxWindow is known.
|
|
91
|
+
*/
|
|
92
|
+
export function sampleWireOverhead(modelId, stateDir, sample, ctxWindow = 0) {
|
|
93
|
+
if (!modelId)
|
|
94
|
+
return 0;
|
|
95
|
+
if (!Number.isFinite(sample) || sample <= 0)
|
|
96
|
+
return 0;
|
|
97
|
+
let ema = sample;
|
|
98
|
+
try {
|
|
99
|
+
const stored = getMetaNumber(overheadKey(modelId), stateDir);
|
|
100
|
+
if (Number.isFinite(stored) && stored > 0) {
|
|
101
|
+
// prev is stored as a fraction ×100; convert back to token space
|
|
102
|
+
// against the window before blending so the EMA is dimensionally
|
|
103
|
+
// consistent (token space in, token space out).
|
|
104
|
+
const prevTokens = (stored / 100) * (ctxWindow > 0 ? ctxWindow : sample);
|
|
105
|
+
ema = OVERHEAD_EMA_ALPHA * sample + (1 - OVERHEAD_EMA_ALPHA) * prevTokens;
|
|
106
|
+
}
|
|
107
|
+
// Convert EMA to a fraction of the window (so the meta value is window-
|
|
108
|
+
// independent + percent-based). When the window is unknown, clamp the
|
|
109
|
+
// stored fraction to a sane [0, OVERHEAD_CLAMP_FRACTION] band so a stale
|
|
110
|
+
// 0-window sample cannot poison a later windowed read.
|
|
111
|
+
let frac = ctxWindow > 0 ? ema / ctxWindow : ema;
|
|
112
|
+
if (ctxWindow > 0) {
|
|
113
|
+
frac = Math.min(frac, OVERHEAD_CLAMP_FRACTION);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
frac = Math.min(Math.max(frac, 0), OVERHEAD_CLAMP_FRACTION);
|
|
117
|
+
}
|
|
118
|
+
setMetaNumber(overheadKey(modelId), Math.round(frac * 100), stateDir);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* non-fatal: best-effort meta write */
|
|
122
|
+
}
|
|
123
|
+
// Return the token-space EMA (clamped) regardless of whether the persist landed.
|
|
124
|
+
let out = ema;
|
|
125
|
+
if (ctxWindow > 0)
|
|
126
|
+
out = Math.min(out, ctxWindow * OVERHEAD_CLAMP_FRACTION);
|
|
127
|
+
return Math.max(0, out);
|
|
128
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { parseWireTruth, sampleWireOverhead, readWireOverhead } from "./wireTruth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Calibrate the overhead EMA from a usage-bearing context event. Called once per
|
|
4
|
+
* event (from the handler) when usage is finite + wireOverhead is ON.
|
|
5
|
+
* `estimateTokens` MUST be the REAL message-list estimate (the engineView
|
|
6
|
+
* estimate), never the pct-derived fallback — pi's percent reconstructs
|
|
7
|
+
* usage.tokens exactly, so using it would force hSample ≈ 0 and erase the EMA
|
|
8
|
+
* (0.6^5 retained after five healthy turns).
|
|
9
|
+
*/
|
|
10
|
+
export function sampleWireOverheadFromUsage(opts) {
|
|
11
|
+
const { runtime, config, modelId, usageTokens, resolvedWindow, estimateTokens } = opts;
|
|
12
|
+
if (!config.wireOverhead)
|
|
13
|
+
return; // byte-identical when OFF
|
|
14
|
+
if (modelId === "" || usageTokens == null || !Number.isFinite(usageTokens) || resolvedWindow <= 0)
|
|
15
|
+
return;
|
|
16
|
+
const hSample = Math.max(0, usageTokens - estimateTokens);
|
|
17
|
+
try {
|
|
18
|
+
sampleWireOverhead(modelId, runtime.currentStateDir, hSample, resolvedWindow);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* non-fatal */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the invisible overhead H (tokens) the handler feeds to the tail-cap
|
|
26
|
+
* budget at the fire/replay paths. Returns the calibrated EMA (or the
|
|
27
|
+
* wireOverheadDefaultPct × window fallback when no sample exists yet). 0 when
|
|
28
|
+
* wireOverhead is OFF or no model/window is known — byte-identical to v0.21.11.
|
|
29
|
+
* Computed once so every call site passes the SAME H the gate used.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveOverheadTokens(opts) {
|
|
32
|
+
const { config, modelId, resolvedWindow, stateDir } = opts;
|
|
33
|
+
if (!config.wireOverhead || modelId === "" || resolvedWindow <= 0)
|
|
34
|
+
return 0;
|
|
35
|
+
const e = readWireOverhead(modelId, stateDir, resolvedWindow);
|
|
36
|
+
return e > 0 ? e : config.wireOverheadDefaultPct * resolvedWindow;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* v0.21.12: invisible-overhead correction of the ESTIMATE-path token count. When
|
|
40
|
+
* the token estimate came from the message list (not provider usage) AND
|
|
41
|
+
* wireOverhead is ON, add H so the gate/thrash/tail-cap see the REAL request
|
|
42
|
+
* size. H defaults to wireOverheadDefaultPct × window until a wire sample
|
|
43
|
+
* calibrates it. Flag OFF ⇒ H = 0 (byte-identical to v0.21.11). The provider
|
|
44
|
+
* usage path is ground truth for the message-list size and is never corrected.
|
|
45
|
+
*/
|
|
46
|
+
export function correctEstimateWithOverhead(opts) {
|
|
47
|
+
const { config, tokenSource, rawTokens, modelId, resolvedWindow, stateDir } = opts;
|
|
48
|
+
if (!config.wireOverhead || tokenSource !== "estimate" || resolvedWindow <= 0)
|
|
49
|
+
return rawTokens;
|
|
50
|
+
const h = modelId !== "" ? readWireOverhead(modelId, stateDir, resolvedWindow) : 0;
|
|
51
|
+
const H = h > 0 ? h : config.wireOverheadDefaultPct * resolvedWindow;
|
|
52
|
+
return rawTokens + H;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Apply the wire-truth gate override for THIS event. Returns the (possibly
|
|
56
|
+
* corrected) currentTokens. When the last assistant message is an error
|
|
57
|
+
* (stopReason "error" or an errorMessage field) and its text matches the
|
|
58
|
+
* provider's overflow error shape, the parsed requestTokens become
|
|
59
|
+
* ground-truth currentTokens — even when our estimate reads far below every
|
|
60
|
+
* threshold. Also feeds the EMA and prefers the parsed availableTokens over
|
|
61
|
+
* runtime.lastCtxWindow when they differ. No-op (returns currentTokens
|
|
62
|
+
* unchanged) when wireOverhead is OFF or no error/parse matched.
|
|
63
|
+
*/
|
|
64
|
+
export function applyWireTruthOverride(opts) {
|
|
65
|
+
const { runtime, config, messages, modelId, resolvedWindow, estimateTokens, currentTokens } = opts;
|
|
66
|
+
if (!config.wireOverhead)
|
|
67
|
+
return currentTokens; // byte-identical when OFF
|
|
68
|
+
try {
|
|
69
|
+
const last = messages[messages.length - 1];
|
|
70
|
+
const lastText = typeof last?.errorMessage === "string"
|
|
71
|
+
? last.errorMessage
|
|
72
|
+
: typeof last?.content === "string"
|
|
73
|
+
? last.content
|
|
74
|
+
: Array.isArray(last?.content)
|
|
75
|
+
? (last.content
|
|
76
|
+
.map((b) => b?.text ?? "")
|
|
77
|
+
.join(""))
|
|
78
|
+
: "";
|
|
79
|
+
const isError = last?.stopReason === "error" || typeof last?.errorMessage === "string";
|
|
80
|
+
if (!isError || lastText.length === 0)
|
|
81
|
+
return currentTokens;
|
|
82
|
+
const parsed = parseWireTruth(lastText);
|
|
83
|
+
if (parsed == null)
|
|
84
|
+
return currentTokens;
|
|
85
|
+
const wireTokens = parsed.requestTokens;
|
|
86
|
+
runtime.lastCtxTokens = wireTokens;
|
|
87
|
+
// EMA: the gap between the wire prompt and our message estimate (usage is
|
|
88
|
+
// absent in the 400 case, so estimateTokens is the true message-list estimate).
|
|
89
|
+
if (modelId !== "" && resolvedWindow > 0) {
|
|
90
|
+
const hSample = Math.max(0, wireTokens - estimateTokens);
|
|
91
|
+
sampleWireOverhead(modelId, runtime.currentStateDir, hSample, resolvedWindow);
|
|
92
|
+
}
|
|
93
|
+
// Prefer the provider's own available size for this event's math.
|
|
94
|
+
if (parsed.availableTokens > 0 &&
|
|
95
|
+
Math.abs(parsed.availableTokens - runtime.lastCtxWindow) > 0) {
|
|
96
|
+
runtime.lastCtxWindow = parsed.availableTokens;
|
|
97
|
+
}
|
|
98
|
+
runtime.diagCtxWireTruth++;
|
|
99
|
+
runtime.logger.info("wire_truth_parse", {
|
|
100
|
+
sessionId: runtime.rt.sessionId,
|
|
101
|
+
requestTokens: parsed.requestTokens,
|
|
102
|
+
availableTokens: parsed.availableTokens,
|
|
103
|
+
estimateTokens,
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
runtime.appendEvent("wire_truth_parse", {
|
|
107
|
+
requestTokens: parsed.requestTokens,
|
|
108
|
+
availableTokens: parsed.availableTokens,
|
|
109
|
+
estimateTokens,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* non-fatal */
|
|
114
|
+
}
|
|
115
|
+
return wireTokens;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* non-fatal */
|
|
119
|
+
return currentTokens;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -10,6 +10,7 @@ import { markCompactionFired, evaluatePendingReduction, } from "./context-handle
|
|
|
10
10
|
import { invokePipeline } from "./context-handler/pipelineRun.js";
|
|
11
11
|
import { buildLiveTrimView } from "./context-handler/liveTrim.js";
|
|
12
12
|
import { recapReplayedTail } from "./context-handler/headroom.js";
|
|
13
|
+
import { sampleWireOverheadFromUsage, applyWireTruthOverride, resolveOverheadTokens, correctEstimateWithOverhead, } from "./context-handler/wireTruthApply.js";
|
|
13
14
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
14
15
|
export function registerContextHandler(pi, runtime, config) {
|
|
15
16
|
// ---- Auto-trigger: live trim (compact and continue) + native durable ----
|
|
@@ -71,13 +72,44 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
71
72
|
// show empty/zero. Compute view lazily only when the fallback is needed
|
|
72
73
|
// (at most one engineView call per context event; when auto is on and
|
|
73
74
|
// usage.tokens is present, view is computed once below via reuse).
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
// v0.21.12: build the engineView whenever wireOverhead is ON (not only when
|
|
76
|
+
// usage is absent) so the EMA sampling + wire-truth blocks measure the REAL
|
|
77
|
+
// message-list estimate. Flag OFF keeps the v0.21.11 lazy path. Without
|
|
78
|
+
// this, estimateTokens falls back to the pct-derived value, which
|
|
79
|
+
// reconstructs usage.tokens exactly → hSample ≈ 0 → EMA trains to nothing.
|
|
80
|
+
const viewForFallback = usage?.tokens == null || config.wireOverhead ? runtime.engineView(messages) : null;
|
|
81
|
+
// v0.21.12: track WHICH source produced currentTokens so the invisible-
|
|
82
|
+
// overhead correction (H) is only applied to the ESTIMATE path. The
|
|
83
|
+
// provider-reported usage is ground truth for the message-list size; H is
|
|
84
|
+
// the gap between that and the wire prompt (system+tools+prepends).
|
|
85
|
+
const tokenSource = usage?.tokens != null
|
|
86
|
+
? "usage"
|
|
87
|
+
: viewForFallback != null
|
|
88
|
+
? "estimate"
|
|
89
|
+
: "pct";
|
|
90
|
+
const estimateTokens = viewForFallback != null
|
|
91
|
+
? estimateSessionTokens(viewForFallback)
|
|
92
|
+
: Math.round(((pct ?? 0) / 100) * (usage?.contextWindow ?? 0));
|
|
93
|
+
const rawTokens = usage?.tokens ?? estimateTokens;
|
|
94
|
+
// v0.21.12: invisible-overhead correction of the ESTIMATE-path count (see
|
|
95
|
+
// wireTruthApply.ts). modelId/resolvedWindow feed the EMA + tail-cap helpers.
|
|
96
|
+
const modelId = runtime.currentModel?.modelId ?? "";
|
|
97
|
+
const resolvedWindow = usage?.contextWindow ?? (runtime.currentModel?.contextWindow ?? 0);
|
|
98
|
+
let currentTokens = correctEstimateWithOverhead({
|
|
99
|
+
config, tokenSource, rawTokens, modelId, resolvedWindow,
|
|
100
|
+
stateDir: runtime.currentStateDir,
|
|
101
|
+
});
|
|
80
102
|
runtime.lastCtxTokens = currentTokens ?? null;
|
|
103
|
+
// v0.21.12: the invisible overhead H to feed the tail-cap budget at the
|
|
104
|
+
// fire/replay paths below (resolved once via the wireTruthApply helper so
|
|
105
|
+
// every call site passes the SAME H the gate used). 0 when the flag is OFF
|
|
106
|
+
// (byte-identical to v0.21.11) or no model/window is known.
|
|
107
|
+
const overheadTokens = resolveOverheadTokens({
|
|
108
|
+
config,
|
|
109
|
+
modelId,
|
|
110
|
+
resolvedWindow,
|
|
111
|
+
stateDir: runtime.currentStateDir,
|
|
112
|
+
});
|
|
81
113
|
// 3WF-2: consume a pending live-window delta from a prior compaction. If a
|
|
82
114
|
// compaction fired on the previous context event and the live window did
|
|
83
115
|
// not shrink, this arms the ThrashGuard (meta). No-op when none pending.
|
|
@@ -103,6 +135,18 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
103
135
|
reportedWindow > 0
|
|
104
136
|
? reportedWindow
|
|
105
137
|
: (runtime.currentModel?.contextWindow ?? 0);
|
|
138
|
+
// v0.21.12: calibrate the invisible overhead H from EVERY context event that
|
|
139
|
+
// carries finite usage. estimateTokens is the REAL message-list estimate
|
|
140
|
+
// (engineView is built when wireOverhead is ON), so hSample = the true
|
|
141
|
+
// overhead (system+tools+prepends), not ≈0. Non-fatal; never throws.
|
|
142
|
+
sampleWireOverheadFromUsage({
|
|
143
|
+
runtime,
|
|
144
|
+
config,
|
|
145
|
+
modelId,
|
|
146
|
+
usageTokens: usage?.tokens,
|
|
147
|
+
resolvedWindow,
|
|
148
|
+
estimateTokens,
|
|
149
|
+
});
|
|
106
150
|
runtime.snapshot(ctx);
|
|
107
151
|
if (!config.auto) {
|
|
108
152
|
const tailed = tailResult();
|
|
@@ -114,6 +158,21 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
114
158
|
// S27 DB-mirror + VC1B ledger append. Runs BEFORE the fast-gate so every
|
|
115
159
|
// message is captured, even if we don't compact this turn. Non-fatal.
|
|
116
160
|
appendMirrorAndLedger(runtime, config, messages);
|
|
161
|
+
// v0.21.12: WIRED-TRUTH gate override (extracted to wireTruthApply.ts).
|
|
162
|
+
// When the last assistant message is an error whose text matches the
|
|
163
|
+
// provider's 400 shape, the parsed requestTokens become ground-truth
|
|
164
|
+
// currentTokens for THIS event's gate — breaking the 400 loop when the
|
|
165
|
+
// estimate path undercounts by ~50% (the v0.21.11 blind spot). No-op when
|
|
166
|
+
// wireOverhead is OFF (byte-identical).
|
|
167
|
+
currentTokens = applyWireTruthOverride({
|
|
168
|
+
runtime,
|
|
169
|
+
config,
|
|
170
|
+
messages,
|
|
171
|
+
modelId,
|
|
172
|
+
resolvedWindow,
|
|
173
|
+
estimateTokens,
|
|
174
|
+
currentTokens,
|
|
175
|
+
});
|
|
117
176
|
// S29 FAST GATE: drive the auto-trigger off the context percent (see
|
|
118
177
|
// gateCheck.ts). Returns a tailed view when the gate does not pass.
|
|
119
178
|
const gate = evaluateGate(runtime, config, { pct, currentTokens, tailResult });
|
|
@@ -156,6 +215,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
156
215
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
157
216
|
outputReservePct: config.outputReservePct,
|
|
158
217
|
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
218
|
+
overheadTokens,
|
|
159
219
|
});
|
|
160
220
|
runtime.diagLiveTrimFires++; // trim view returned this call (replay counts as a fire)
|
|
161
221
|
runtime.diagLiveTrimReplays++;
|
|
@@ -203,6 +263,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
203
263
|
pct,
|
|
204
264
|
currentTokens,
|
|
205
265
|
tailResult,
|
|
266
|
+
overheadTokens,
|
|
206
267
|
});
|
|
207
268
|
if (pipeline.kind === "return")
|
|
208
269
|
return pipeline.view;
|
|
@@ -286,6 +347,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
286
347
|
ran: pipeline.ran,
|
|
287
348
|
perModelThreshold: gate.perModelThreshold,
|
|
288
349
|
tailResult,
|
|
350
|
+
overheadTokens,
|
|
289
351
|
});
|
|
290
352
|
});
|
|
291
353
|
}
|
|
@@ -24,6 +24,8 @@ export class RuntimeInstrumentation {
|
|
|
24
24
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
25
25
|
diagCtxOutputErrorTrip = 0; // Phase H: output-error catch tripped a forced compaction
|
|
26
26
|
diagCtxHeadroomTrip = 0; // v0.21.9: output-headroom gate tripped a pre-overflow compaction
|
|
27
|
+
diagCtxWireTruth = 0; // v0.21.12: a provider 400 text was parsed into ground-truth tokens
|
|
28
|
+
diagCtxSkipCapped = 0; // v0.21.12: the live-trim skip path was tail-capped to fit the budget
|
|
27
29
|
// Context health instrumentation (v0.12): rolling ring buffers for
|
|
28
30
|
// drift detection + cache poison Layer 1 hash baseline.
|
|
29
31
|
recentTurnEmbeddings = [];
|
|
@@ -87,5 +87,19 @@ export const COMPACTION_SETTINGS: SettingGroup = {
|
|
|
87
87
|
0.1,
|
|
88
88
|
0.95,
|
|
89
89
|
),
|
|
90
|
+
boolDirect(
|
|
91
|
+
"MEGACOMPACT_WIRE_OVERHEAD",
|
|
92
|
+
"Invisible-Overhead Calibration",
|
|
93
|
+
"Add the provider's fixed request overhead H (system prompt + tool definitions + extension systemPrompt prepends — everything pi adds at request time that NEVER appears in the stored transcript) back into the token estimate for the headroom gate + tail cap. H is a per-model EMA of observed wire samples, else the Wire-Overhead Default fraction of the window. Closes the 32k overflow loop (attempt #9). OFF = byte-identical v0.21.11.",
|
|
94
|
+
true,
|
|
95
|
+
),
|
|
96
|
+
num(
|
|
97
|
+
"MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT",
|
|
98
|
+
"Wire-Overhead Default %",
|
|
99
|
+
"Fraction of the context window used as the overhead H when no wire sample has been observed yet for the model. Once a sample lands, the per-model EMA wins. Clamped 0–0.85, default 0.15 (15%). Percent-based: identical math at every window size.",
|
|
100
|
+
0.15,
|
|
101
|
+
0,
|
|
102
|
+
0.85,
|
|
103
|
+
),
|
|
90
104
|
],
|
|
91
105
|
};
|
|
@@ -233,4 +233,18 @@ export interface MegaConfig {
|
|
|
233
233
|
contextHealthCachePoison: boolean;
|
|
234
234
|
/** v0.12: KV cache poison mitigation — inject prefix break on mismatch. Default OFF. */
|
|
235
235
|
contextHealthMitigate: boolean;
|
|
236
|
+
/** v0.21.12: invisible-overhead calibration. When ON, the handler adds the
|
|
237
|
+
* provider's fixed request overhead H (system prompt + tool definitions +
|
|
238
|
+
* extension systemPrompt prepends — everything pi adds at request time that
|
|
239
|
+
* NEVER appears in the stored transcript) back into the token estimate used
|
|
240
|
+
* by the headroom gate, thrash consult, and tail-cap budget. H is calibrated
|
|
241
|
+
* per-model from observed wire samples (EMA) when available, else a
|
|
242
|
+
* configurable fraction of the window (wireOverheadDefaultPct). OFF =
|
|
243
|
+
* byte-identical to v0.21.11 (every H term is 0; no code path changes). */
|
|
244
|
+
wireOverhead: boolean;
|
|
245
|
+
/** v0.21.12: fraction of the context window used as H when no EMA sample
|
|
246
|
+
* exists yet for the model. Clamped [0, 0.85]; default 0.15. Percent-based by
|
|
247
|
+
* design (LTS invariant): identical math at every window size. Only the
|
|
248
|
+
* no-sample fallback — once a wire sample lands, the EMA wins. */
|
|
249
|
+
wireOverheadDefaultPct: number;
|
|
236
250
|
}
|
|
@@ -302,6 +302,16 @@ export function loadConfig(): MegaConfig {
|
|
|
302
302
|
contextHealthOutputQuality: envBool("MEGACOMPACT_CONTEXT_HEALTH_OUTPUT_QUALITY", true),
|
|
303
303
|
contextHealthCachePoison: envBool("MEGACOMPACT_CONTEXT_HEALTH_CACHE_POISON", true),
|
|
304
304
|
contextHealthMitigate: envBool("MEGACOMPACT_CONTEXT_HEALTH_MITIGATE", false),
|
|
305
|
+
// v0.21.12: invisible-overhead calibration — add the provider's fixed
|
|
306
|
+
// request overhead H (system+tools+extension prepends, never in the
|
|
307
|
+
// transcript) back into the token estimate for the headroom gate / tail
|
|
308
|
+
// cap. H is an EMA of observed wire samples per model, else
|
|
309
|
+
// wireOverheadDefaultPct × window. Default ON; OFF = byte-identical
|
|
310
|
+
// v0.21.11 (every H term is 0). Closes attempt #9 of the 32k overflow loop.
|
|
311
|
+
wireOverhead: envBool("MEGACOMPACT_WIRE_OVERHEAD", true),
|
|
312
|
+
// v0.21.12: fallback H as a fraction of the window when no EMA sample
|
|
313
|
+
// exists yet. Clamped [0, 0.85]; default 0.15. Percent-based.
|
|
314
|
+
wireOverheadDefaultPct: clamp(envFlag("MEGACOMPACT_WIRE_OVERHEAD_DEFAULT_PCT", 0.15), 0, 0.85),
|
|
305
315
|
// D.1: env-overridable recompact delta (minimum context growth % before
|
|
306
316
|
// re-compacting instead of replaying the cached live trim). Default 50.
|
|
307
317
|
recompactPctDelta: envFlag("MEGACOMPACT_RECOMPACT_PCT_DELTA", 50),
|
|
@@ -159,6 +159,15 @@ export function applyTailCap(opts: {
|
|
|
159
159
|
* otherwise each message's tokens are estimated from its text content.
|
|
160
160
|
*/
|
|
161
161
|
messageTokens?: readonly number[];
|
|
162
|
+
/**
|
|
163
|
+
* v0.21.12: the provider's invisible overhead H (system prompt + tool
|
|
164
|
+
* definitions + extension systemPrompt prepends — everything pi adds at
|
|
165
|
+
* request time that NEVER appears in the stored transcript). Subtracted from
|
|
166
|
+
* the budget so the cap accounts for the REAL wire prompt, not just the
|
|
167
|
+
* counted messages. Default 0 (no H) — identical behavior to v0.21.11 when
|
|
168
|
+
* the wireOverhead flag is OFF. Clamped to >= 0.
|
|
169
|
+
*/
|
|
170
|
+
overheadTokens?: number;
|
|
162
171
|
}): { recent: AgentMessage[]; dropped: number } {
|
|
163
172
|
const { recentRaw, summaryTokens, ctxWindow, outputReservePct } = opts;
|
|
164
173
|
if (ctxWindow <= 0 || recentRaw.length <= 1) {
|
|
@@ -180,9 +189,16 @@ export function applyTailCap(opts: {
|
|
|
180
189
|
// <= 95% of the window) plus margin + summary can still exceed the window
|
|
181
190
|
// on tiny summaries-free edges; the floor keeps the cap alive with a small
|
|
182
191
|
// positive budget instead of disabling it (pre-v0.21.9 behavior).
|
|
192
|
+
// v0.21.12: also subtract the invisible overhead H so the cap bounds the
|
|
193
|
+
// ACTUAL wire prompt (messages + H), breaking the 400 loop when the estimate
|
|
194
|
+
// undercounts by the system-prompt/tool-definition overhead.
|
|
183
195
|
const budget = Math.max(
|
|
184
196
|
1,
|
|
185
|
-
ctxWindow -
|
|
197
|
+
ctxWindow -
|
|
198
|
+
reserveTokens -
|
|
199
|
+
safetyMargin -
|
|
200
|
+
Math.max(0, summaryTokens) -
|
|
201
|
+
Math.max(0, opts.overheadTokens ?? 0),
|
|
186
202
|
);
|
|
187
203
|
let start = 0;
|
|
188
204
|
let tailTokens = 0;
|
|
@@ -225,6 +241,8 @@ export function recapReplayedTail(opts: {
|
|
|
225
241
|
maxOutputTokens: number;
|
|
226
242
|
outputReservePct: number;
|
|
227
243
|
safetyMarginPct: number;
|
|
244
|
+
/** v0.21.12: invisible overhead H (see applyTailCap). Default 0. */
|
|
245
|
+
overheadTokens?: number;
|
|
228
246
|
}): { recent: AgentMessage[]; dropped: number } {
|
|
229
247
|
return applyTailCap({
|
|
230
248
|
recentRaw: opts.recentRaw,
|
|
@@ -233,5 +251,6 @@ export function recapReplayedTail(opts: {
|
|
|
233
251
|
maxOutputTokens: opts.maxOutputTokens,
|
|
234
252
|
outputReservePct: opts.outputReservePct,
|
|
235
253
|
safetyMarginPct: opts.safetyMarginPct,
|
|
254
|
+
overheadTokens: opts.overheadTokens ?? 0,
|
|
236
255
|
});
|
|
237
256
|
}
|
|
@@ -45,6 +45,10 @@ export function buildLiveTrimView(
|
|
|
45
45
|
ran: { result: CompactResult };
|
|
46
46
|
perModelThreshold: { safetyMarginPct: number; firePointPct: number };
|
|
47
47
|
tailResult: TailResultFn;
|
|
48
|
+
/** v0.21.12: the provider's invisible overhead H (system+tools+prepends),
|
|
49
|
+
* handler-resolved. Subtracted from the tail-cap budget so the cap bounds
|
|
50
|
+
* the REAL wire prompt. 0 when the flag is OFF (byte-identical v0.21.11). */
|
|
51
|
+
overheadTokens?: number;
|
|
48
52
|
},
|
|
49
53
|
): { messages: AgentMessage[] } | undefined {
|
|
50
54
|
const {
|
|
@@ -57,6 +61,7 @@ export function buildLiveTrimView(
|
|
|
57
61
|
ran,
|
|
58
62
|
perModelThreshold,
|
|
59
63
|
tailResult,
|
|
64
|
+
overheadTokens = 0,
|
|
60
65
|
} = opts;
|
|
61
66
|
|
|
62
67
|
// S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
|
|
@@ -105,6 +110,37 @@ export function buildLiveTrimView(
|
|
|
105
110
|
anchorUserMessages,
|
|
106
111
|
criticalOver: (pct ?? 0) >= 90,
|
|
107
112
|
});
|
|
113
|
+
// v0.21.12: CAP THE SKIP PATH. When computeLiveTrimCut returns null
|
|
114
|
+
// (anchor floor blocked cutting a fat recent tool pair, or the
|
|
115
|
+
// criticalOver hatch stayed closed because estimated pressure ≈80%),
|
|
116
|
+
// the pre-v0.21.12 code shipped the RAW untrimmed view — which, with
|
|
117
|
+
// the invisible overhead H uncounted, overflowed the window → 400
|
|
118
|
+
// again, forever (the entire v0.21.11 blind spot). This is the
|
|
119
|
+
// invariant "the trim path never ships a view the budget wouldn't
|
|
120
|
+
// allow": even when we cannot summarize, we still front-drop
|
|
121
|
+
// OLDEST messages until the RAW tail + overhead fits the budget, so
|
|
122
|
+
// the model is never fed a prompt that exceeds its window. Flag OFF
|
|
123
|
+
// ⇒ byte-identical to v0.21.11 (return raw, no cap).
|
|
124
|
+
if (config.wireOverhead && runtime.lastCtxWindow > 0) {
|
|
125
|
+
const { recent, dropped } = applyTailCap({
|
|
126
|
+
recentRaw: messages,
|
|
127
|
+
summaryTokens: 0,
|
|
128
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
129
|
+
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
130
|
+
outputReservePct: config.outputReservePct,
|
|
131
|
+
safetyMarginPct: perModelThreshold.safetyMarginPct,
|
|
132
|
+
overheadTokens,
|
|
133
|
+
});
|
|
134
|
+
if (dropped > 0) {
|
|
135
|
+
runtime.diagCtxSkipCapped++;
|
|
136
|
+
runtime.logger.info("skip_cap_applied", {
|
|
137
|
+
sessionId: runtime.rt.sessionId,
|
|
138
|
+
dropped,
|
|
139
|
+
ctxWindow: runtime.lastCtxWindow,
|
|
140
|
+
});
|
|
141
|
+
return tailResult(recent) ?? { messages: recent };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
108
144
|
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
109
145
|
}
|
|
110
146
|
const summaryMsg = liveTrimSummaryMessage({
|
|
@@ -152,6 +188,7 @@ export function buildLiveTrimView(
|
|
|
152
188
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
153
189
|
outputReservePct: config.outputReservePct,
|
|
154
190
|
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
191
|
+
overheadTokens,
|
|
155
192
|
});
|
|
156
193
|
if (dropped > 0) {
|
|
157
194
|
runtime.logger.warn("live-trim-tail-cap", {
|
|
@@ -48,6 +48,7 @@ export function invokePipeline(
|
|
|
48
48
|
pct: number | null | undefined;
|
|
49
49
|
currentTokens: number;
|
|
50
50
|
tailResult: TailResultFn;
|
|
51
|
+
overheadTokens?: number;
|
|
51
52
|
},
|
|
52
53
|
): PipelineOutcome {
|
|
53
54
|
// VC5C: emit the rollout decision per compact event (observability seam).
|
|
@@ -133,6 +134,7 @@ export function invokePipeline(
|
|
|
133
134
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
134
135
|
outputReservePct: config.outputReservePct,
|
|
135
136
|
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
137
|
+
overheadTokens: opts.overheadTokens ?? 0,
|
|
136
138
|
});
|
|
137
139
|
runtime.diagLiveTrimFires++;
|
|
138
140
|
runtime.diagLiveTrimReplays++;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/wireTruth.ts — invisible-overhead calibration + wire-truth parse.
|
|
3
|
+
*
|
|
4
|
+
* Attempt #9 on the small-context-model overflow loop (2026-08-19 incident,
|
|
5
|
+
* 8 prior attempts). ROOT CAUSE: pi adds a FIXED OVERHEAD H at request time
|
|
6
|
+
* (system prompt + tool definitions + extension systemPrompt prepends) that
|
|
7
|
+
* NEVER appears in the stored transcript. Neither pi's estimateContextTokens nor
|
|
8
|
+
* our estimateSessionTokens/applyTailCap count H. So when the provider 400s with
|
|
9
|
+
* "request (39048 tokens) exceeds the available context size (32768 tokens)" we
|
|
10
|
+
* have been estimating ~18–26k and judging headroom against that — undercounting
|
|
11
|
+
* by ~50%, so the gate never trips correctly and a RAW uncapped view ships → 400
|
|
12
|
+
* again, forever (pi's one-shot overflow recovery only resets on user
|
|
13
|
+
* message_start, so auto-retries burn it permanently).
|
|
14
|
+
*
|
|
15
|
+
* Two mechanisms here:
|
|
16
|
+
* - parseWireTruth: regex the provider's 400 text into ground-truth
|
|
17
|
+
* request/available token counts. Pure + unit-tested.
|
|
18
|
+
* - Per-model overhead EMA: calibrate H from observed samples so the gate and
|
|
19
|
+
* tail cap can ADD it back. Persisted in the SQLite meta table (copy the
|
|
20
|
+
* thrashGuard.ts pattern) so it survives restarts and travels with the repo.
|
|
21
|
+
*
|
|
22
|
+
* Non-fatal EVERYWHERE: every store read/write is best-effort, swallowed on
|
|
23
|
+
* failure. Structured JSON logging only (logger + emit dual-sink, mirroring
|
|
24
|
+
* armThrashGuard's 3WF-5 pattern). No console.*, no network, no mocks.
|
|
25
|
+
*/
|
|
26
|
+
import { getMetaNumber, setMetaNumber } from "../../../src/store/sqlite.js";
|
|
27
|
+
|
|
28
|
+
/** EMA smoothing factor for the overhead calibration (fixed, no count needed). */
|
|
29
|
+
export const OVERHEAD_EMA_ALPHA = 0.4;
|
|
30
|
+
|
|
31
|
+
/** Safety clamp: an overhead sample may never exceed this fraction of the window. */
|
|
32
|
+
export const OVERHEAD_CLAMP_FRACTION = 0.85;
|
|
33
|
+
|
|
34
|
+
/** Meta key prefix; the model id is appended (meta stores integers only). */
|
|
35
|
+
export const OVERHEAD_META_PREFIX = "wire.overhead_ema.";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse the provider's overflow error text into ground-truth token counts.
|
|
39
|
+
*
|
|
40
|
+
* Matches strings like:
|
|
41
|
+
* "request (39048 tokens) exceeds the available context size (32768 tokens)"
|
|
42
|
+
* "request (39,048 tokens) exceeds the available context size (32,768 tokens)"
|
|
43
|
+
*
|
|
44
|
+
* Pure + side-effect free — trivially unit-testable. Returns null when the text
|
|
45
|
+
* does not match the expected shape (e.g. an unrelated error message).
|
|
46
|
+
*/
|
|
47
|
+
export function parseWireTruth(
|
|
48
|
+
text: string,
|
|
49
|
+
): { requestTokens: number; availableTokens: number } | null {
|
|
50
|
+
if (typeof text !== "string" || text.length === 0) return null;
|
|
51
|
+
const m = text.match(
|
|
52
|
+
/request\s*\((\d[\d,]*)\s*tokens\)\s*exceeds the available context size\s*\((\d[\d,]*)\s*tokens\)/i,
|
|
53
|
+
);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const requestTokens = Number(m[1]?.replace(/,/g, ""));
|
|
56
|
+
const availableTokens = Number(m[2]?.replace(/,/g, ""));
|
|
57
|
+
if (!Number.isFinite(requestTokens) || !Number.isFinite(availableTokens)) return null;
|
|
58
|
+
if (requestTokens <= 0 || availableTokens <= 0) return null;
|
|
59
|
+
return { requestTokens, availableTokens };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build the meta key for a model's overhead EMA (stored ×100, integer). */
|
|
63
|
+
function overheadKey(modelId: string): string {
|
|
64
|
+
return OVERHEAD_META_PREFIX + modelId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read the calibrated overhead (tokens) for a model. Returns 0 when absent or
|
|
69
|
+
* unreadable — non-fatal everywhere. When the context window is known, clamps the
|
|
70
|
+
* value into [0, 0.85 × ctxWindow] as a safety against a runaway EMA.
|
|
71
|
+
*/
|
|
72
|
+
export function readWireOverhead(modelId: string, stateDir: string, ctxWindow = 0): number {
|
|
73
|
+
if (!modelId) return 0;
|
|
74
|
+
try {
|
|
75
|
+
const stored = getMetaNumber(overheadKey(modelId), stateDir);
|
|
76
|
+
if (!Number.isFinite(stored) || stored <= 0) return 0;
|
|
77
|
+
const frac = stored / 100;
|
|
78
|
+
let overhead = frac * (ctxWindow > 0 ? ctxWindow : 1);
|
|
79
|
+
if (ctxWindow > 0) {
|
|
80
|
+
overhead = Math.min(overhead, ctxWindow * OVERHEAD_CLAMP_FRACTION);
|
|
81
|
+
}
|
|
82
|
+
return Math.max(0, overhead);
|
|
83
|
+
} catch {
|
|
84
|
+
return 0; // non-fatal: never fail the agent loop on a store read
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Fold a new overhead sample into the per-model EMA and persist it. Returns the
|
|
90
|
+
* new EMA in tokens (0 when the sample is invalid). First sample initializes the
|
|
91
|
+
* EMA directly (no warm-up). Best-effort: never throws; a store failure is
|
|
92
|
+
* swallowed and the in-memory EMA is still returned.
|
|
93
|
+
*
|
|
94
|
+
* Clamped into [0, 0.85 × ctxWindow] when ctxWindow is known.
|
|
95
|
+
*/
|
|
96
|
+
export function sampleWireOverhead(
|
|
97
|
+
modelId: string,
|
|
98
|
+
stateDir: string,
|
|
99
|
+
sample: number,
|
|
100
|
+
ctxWindow = 0,
|
|
101
|
+
): number {
|
|
102
|
+
if (!modelId) return 0;
|
|
103
|
+
if (!Number.isFinite(sample) || sample <= 0) return 0;
|
|
104
|
+
let ema = sample;
|
|
105
|
+
try {
|
|
106
|
+
const stored = getMetaNumber(overheadKey(modelId), stateDir);
|
|
107
|
+
if (Number.isFinite(stored) && stored > 0) {
|
|
108
|
+
// prev is stored as a fraction ×100; convert back to token space
|
|
109
|
+
// against the window before blending so the EMA is dimensionally
|
|
110
|
+
// consistent (token space in, token space out).
|
|
111
|
+
const prevTokens = (stored / 100) * (ctxWindow > 0 ? ctxWindow : sample);
|
|
112
|
+
ema = OVERHEAD_EMA_ALPHA * sample + (1 - OVERHEAD_EMA_ALPHA) * prevTokens;
|
|
113
|
+
}
|
|
114
|
+
// Convert EMA to a fraction of the window (so the meta value is window-
|
|
115
|
+
// independent + percent-based). When the window is unknown, clamp the
|
|
116
|
+
// stored fraction to a sane [0, OVERHEAD_CLAMP_FRACTION] band so a stale
|
|
117
|
+
// 0-window sample cannot poison a later windowed read.
|
|
118
|
+
let frac = ctxWindow > 0 ? ema / ctxWindow : ema;
|
|
119
|
+
if (ctxWindow > 0) {
|
|
120
|
+
frac = Math.min(frac, OVERHEAD_CLAMP_FRACTION);
|
|
121
|
+
} else {
|
|
122
|
+
frac = Math.min(Math.max(frac, 0), OVERHEAD_CLAMP_FRACTION);
|
|
123
|
+
}
|
|
124
|
+
setMetaNumber(overheadKey(modelId), Math.round(frac * 100), stateDir);
|
|
125
|
+
} catch {
|
|
126
|
+
/* non-fatal: best-effort meta write */
|
|
127
|
+
}
|
|
128
|
+
// Return the token-space EMA (clamped) regardless of whether the persist landed.
|
|
129
|
+
let out = ema;
|
|
130
|
+
if (ctxWindow > 0) out = Math.min(out, ctxWindow * OVERHEAD_CLAMP_FRACTION);
|
|
131
|
+
return Math.max(0, out);
|
|
132
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/wireTruthApply.ts — v0.21.12 wired-overhead runtime seam.
|
|
3
|
+
*
|
|
4
|
+
* Delegate-shell sibling extracted from context-handler.ts (extensions/ 400-line
|
|
5
|
+
* soft limit). Holds the two wire-overhead call-site blocks that don't fit the
|
|
6
|
+
* handler's budget:
|
|
7
|
+
* - sampleWireOverheadFromUsage: calibrate the per-model overhead EMA from a
|
|
8
|
+
* healthy usage-bearing context event (H_sample = usage.tokens − the REAL
|
|
9
|
+
* message-list estimate). MUST use the true estimate, not the pct-derived
|
|
10
|
+
* fallback — otherwise hSample ≈ 0 and the EMA trains itself to nothing.
|
|
11
|
+
* - applyWireTruthOverride: when the last assistant message is an error whose
|
|
12
|
+
* text matches the provider's 400 shape, treat the parsed requestTokens as
|
|
13
|
+
* ground-truth currentTokens for THIS event's gate, feed the EMA, and prefer
|
|
14
|
+
* the parsed availableTokens over runtime.lastCtxWindow.
|
|
15
|
+
*
|
|
16
|
+
* Both are no-op / byte-identical when config.wireOverhead is OFF. Non-fatal
|
|
17
|
+
* everywhere; never throw on the agent loop. Structured logging only.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
20
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
21
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
22
|
+
import { parseWireTruth, sampleWireOverhead, readWireOverhead } from "./wireTruth.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calibrate the overhead EMA from a usage-bearing context event. Called once per
|
|
26
|
+
* event (from the handler) when usage is finite + wireOverhead is ON.
|
|
27
|
+
* `estimateTokens` MUST be the REAL message-list estimate (the engineView
|
|
28
|
+
* estimate), never the pct-derived fallback — pi's percent reconstructs
|
|
29
|
+
* usage.tokens exactly, so using it would force hSample ≈ 0 and erase the EMA
|
|
30
|
+
* (0.6^5 retained after five healthy turns).
|
|
31
|
+
*/
|
|
32
|
+
export function sampleWireOverheadFromUsage(opts: {
|
|
33
|
+
runtime: MegaRuntime;
|
|
34
|
+
config: MegaConfig;
|
|
35
|
+
modelId: string;
|
|
36
|
+
usageTokens: number | null | undefined;
|
|
37
|
+
resolvedWindow: number;
|
|
38
|
+
estimateTokens: number;
|
|
39
|
+
}): void {
|
|
40
|
+
const { runtime, config, modelId, usageTokens, resolvedWindow, estimateTokens } = opts;
|
|
41
|
+
if (!config.wireOverhead) return; // byte-identical when OFF
|
|
42
|
+
if (modelId === "" || usageTokens == null || !Number.isFinite(usageTokens) || resolvedWindow <= 0)
|
|
43
|
+
return;
|
|
44
|
+
const hSample = Math.max(0, usageTokens - estimateTokens);
|
|
45
|
+
try {
|
|
46
|
+
sampleWireOverhead(modelId, runtime.currentStateDir, hSample, resolvedWindow);
|
|
47
|
+
} catch {
|
|
48
|
+
/* non-fatal */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the invisible overhead H (tokens) the handler feeds to the tail-cap
|
|
54
|
+
* budget at the fire/replay paths. Returns the calibrated EMA (or the
|
|
55
|
+
* wireOverheadDefaultPct × window fallback when no sample exists yet). 0 when
|
|
56
|
+
* wireOverhead is OFF or no model/window is known — byte-identical to v0.21.11.
|
|
57
|
+
* Computed once so every call site passes the SAME H the gate used.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveOverheadTokens(opts: {
|
|
60
|
+
config: MegaConfig;
|
|
61
|
+
modelId: string;
|
|
62
|
+
resolvedWindow: number;
|
|
63
|
+
stateDir: string;
|
|
64
|
+
}): number {
|
|
65
|
+
const { config, modelId, resolvedWindow, stateDir } = opts;
|
|
66
|
+
if (!config.wireOverhead || modelId === "" || resolvedWindow <= 0) return 0;
|
|
67
|
+
const e = readWireOverhead(modelId, stateDir, resolvedWindow);
|
|
68
|
+
return e > 0 ? e : config.wireOverheadDefaultPct * resolvedWindow;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* v0.21.12: invisible-overhead correction of the ESTIMATE-path token count. When
|
|
73
|
+
* the token estimate came from the message list (not provider usage) AND
|
|
74
|
+
* wireOverhead is ON, add H so the gate/thrash/tail-cap see the REAL request
|
|
75
|
+
* size. H defaults to wireOverheadDefaultPct × window until a wire sample
|
|
76
|
+
* calibrates it. Flag OFF ⇒ H = 0 (byte-identical to v0.21.11). The provider
|
|
77
|
+
* usage path is ground truth for the message-list size and is never corrected.
|
|
78
|
+
*/
|
|
79
|
+
export function correctEstimateWithOverhead(opts: {
|
|
80
|
+
config: MegaConfig;
|
|
81
|
+
tokenSource: "usage" | "estimate" | "pct";
|
|
82
|
+
rawTokens: number;
|
|
83
|
+
modelId: string;
|
|
84
|
+
resolvedWindow: number;
|
|
85
|
+
stateDir: string;
|
|
86
|
+
}): number {
|
|
87
|
+
const { config, tokenSource, rawTokens, modelId, resolvedWindow, stateDir } = opts;
|
|
88
|
+
if (!config.wireOverhead || tokenSource !== "estimate" || resolvedWindow <= 0) return rawTokens;
|
|
89
|
+
const h = modelId !== "" ? readWireOverhead(modelId, stateDir, resolvedWindow) : 0;
|
|
90
|
+
const H = h > 0 ? h : config.wireOverheadDefaultPct * resolvedWindow;
|
|
91
|
+
return rawTokens + H;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply the wire-truth gate override for THIS event. Returns the (possibly
|
|
96
|
+
* corrected) currentTokens. When the last assistant message is an error
|
|
97
|
+
* (stopReason "error" or an errorMessage field) and its text matches the
|
|
98
|
+
* provider's overflow error shape, the parsed requestTokens become
|
|
99
|
+
* ground-truth currentTokens — even when our estimate reads far below every
|
|
100
|
+
* threshold. Also feeds the EMA and prefers the parsed availableTokens over
|
|
101
|
+
* runtime.lastCtxWindow when they differ. No-op (returns currentTokens
|
|
102
|
+
* unchanged) when wireOverhead is OFF or no error/parse matched.
|
|
103
|
+
*/
|
|
104
|
+
export function applyWireTruthOverride(opts: {
|
|
105
|
+
runtime: MegaRuntime;
|
|
106
|
+
config: MegaConfig;
|
|
107
|
+
messages: readonly AgentMessage[];
|
|
108
|
+
modelId: string;
|
|
109
|
+
resolvedWindow: number;
|
|
110
|
+
estimateTokens: number;
|
|
111
|
+
currentTokens: number;
|
|
112
|
+
}): number {
|
|
113
|
+
const { runtime, config, messages, modelId, resolvedWindow, estimateTokens, currentTokens } = opts;
|
|
114
|
+
if (!config.wireOverhead) return currentTokens; // byte-identical when OFF
|
|
115
|
+
try {
|
|
116
|
+
const last = messages[messages.length - 1] as
|
|
117
|
+
| { role?: string; stopReason?: string; errorMessage?: string; content?: unknown }
|
|
118
|
+
| undefined;
|
|
119
|
+
const lastText =
|
|
120
|
+
typeof last?.errorMessage === "string"
|
|
121
|
+
? last.errorMessage
|
|
122
|
+
: typeof last?.content === "string"
|
|
123
|
+
? last.content
|
|
124
|
+
: Array.isArray(last?.content)
|
|
125
|
+
? ((last.content as Array<{ text?: string }>)
|
|
126
|
+
.map((b) => b?.text ?? "")
|
|
127
|
+
.join(""))
|
|
128
|
+
: "";
|
|
129
|
+
const isError =
|
|
130
|
+
last?.stopReason === "error" || typeof last?.errorMessage === "string";
|
|
131
|
+
if (!isError || lastText.length === 0) return currentTokens;
|
|
132
|
+
const parsed = parseWireTruth(lastText);
|
|
133
|
+
if (parsed == null) return currentTokens;
|
|
134
|
+
const wireTokens = parsed.requestTokens;
|
|
135
|
+
runtime.lastCtxTokens = wireTokens;
|
|
136
|
+
// EMA: the gap between the wire prompt and our message estimate (usage is
|
|
137
|
+
// absent in the 400 case, so estimateTokens is the true message-list estimate).
|
|
138
|
+
if (modelId !== "" && resolvedWindow > 0) {
|
|
139
|
+
const hSample = Math.max(0, wireTokens - estimateTokens);
|
|
140
|
+
sampleWireOverhead(modelId, runtime.currentStateDir, hSample, resolvedWindow);
|
|
141
|
+
}
|
|
142
|
+
// Prefer the provider's own available size for this event's math.
|
|
143
|
+
if (parsed.availableTokens > 0 &&
|
|
144
|
+
Math.abs(parsed.availableTokens - runtime.lastCtxWindow) > 0) {
|
|
145
|
+
runtime.lastCtxWindow = parsed.availableTokens;
|
|
146
|
+
}
|
|
147
|
+
runtime.diagCtxWireTruth++;
|
|
148
|
+
runtime.logger.info("wire_truth_parse", {
|
|
149
|
+
sessionId: runtime.rt.sessionId,
|
|
150
|
+
requestTokens: parsed.requestTokens,
|
|
151
|
+
availableTokens: parsed.availableTokens,
|
|
152
|
+
estimateTokens,
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
runtime.appendEvent("wire_truth_parse", {
|
|
156
|
+
requestTokens: parsed.requestTokens,
|
|
157
|
+
availableTokens: parsed.availableTokens,
|
|
158
|
+
estimateTokens,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
/* non-fatal */
|
|
162
|
+
}
|
|
163
|
+
return wireTokens;
|
|
164
|
+
} catch {
|
|
165
|
+
/* non-fatal */
|
|
166
|
+
return currentTokens;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -35,6 +35,12 @@ import {
|
|
|
35
35
|
import { invokePipeline } from "./context-handler/pipelineRun.js";
|
|
36
36
|
import { buildLiveTrimView } from "./context-handler/liveTrim.js";
|
|
37
37
|
import { recapReplayedTail } from "./context-handler/headroom.js";
|
|
38
|
+
import {
|
|
39
|
+
sampleWireOverheadFromUsage,
|
|
40
|
+
applyWireTruthOverride,
|
|
41
|
+
resolveOverheadTokens,
|
|
42
|
+
correctEstimateWithOverhead,
|
|
43
|
+
} from "./context-handler/wireTruthApply.js";
|
|
38
44
|
|
|
39
45
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
40
46
|
export function registerContextHandler(
|
|
@@ -99,15 +105,48 @@ export function registerContextHandler(
|
|
|
99
105
|
// show empty/zero. Compute view lazily only when the fallback is needed
|
|
100
106
|
// (at most one engineView call per context event; when auto is on and
|
|
101
107
|
// usage.tokens is present, view is computed once below via reuse).
|
|
108
|
+
// v0.21.12: build the engineView whenever wireOverhead is ON (not only when
|
|
109
|
+
// usage is absent) so the EMA sampling + wire-truth blocks measure the REAL
|
|
110
|
+
// message-list estimate. Flag OFF keeps the v0.21.11 lazy path. Without
|
|
111
|
+
// this, estimateTokens falls back to the pct-derived value, which
|
|
112
|
+
// reconstructs usage.tokens exactly → hSample ≈ 0 → EMA trains to nothing.
|
|
102
113
|
const viewForFallback =
|
|
103
|
-
usage?.tokens == null ? runtime.engineView(messages) : null;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
usage?.tokens == null || config.wireOverhead ? runtime.engineView(messages) : null;
|
|
115
|
+
// v0.21.12: track WHICH source produced currentTokens so the invisible-
|
|
116
|
+
// overhead correction (H) is only applied to the ESTIMATE path. The
|
|
117
|
+
// provider-reported usage is ground truth for the message-list size; H is
|
|
118
|
+
// the gap between that and the wire prompt (system+tools+prepends).
|
|
119
|
+
const tokenSource: "usage" | "estimate" | "pct" =
|
|
120
|
+
usage?.tokens != null
|
|
121
|
+
? "usage"
|
|
122
|
+
: viewForFallback != null
|
|
123
|
+
? "estimate"
|
|
124
|
+
: "pct";
|
|
125
|
+
const estimateTokens =
|
|
126
|
+
viewForFallback != null
|
|
107
127
|
? estimateSessionTokens(viewForFallback)
|
|
108
|
-
:
|
|
109
|
-
|
|
128
|
+
: Math.round(((pct ?? 0) / 100) * (usage?.contextWindow ?? 0));
|
|
129
|
+
const rawTokens = usage?.tokens ?? estimateTokens;
|
|
130
|
+
// v0.21.12: invisible-overhead correction of the ESTIMATE-path count (see
|
|
131
|
+
// wireTruthApply.ts). modelId/resolvedWindow feed the EMA + tail-cap helpers.
|
|
132
|
+
const modelId = runtime.currentModel?.modelId ?? "";
|
|
133
|
+
const resolvedWindow =
|
|
134
|
+
usage?.contextWindow ?? (runtime.currentModel?.contextWindow ?? 0);
|
|
135
|
+
let currentTokens = correctEstimateWithOverhead({
|
|
136
|
+
config, tokenSource, rawTokens, modelId, resolvedWindow,
|
|
137
|
+
stateDir: runtime.currentStateDir,
|
|
138
|
+
});
|
|
110
139
|
runtime.lastCtxTokens = currentTokens ?? null;
|
|
140
|
+
// v0.21.12: the invisible overhead H to feed the tail-cap budget at the
|
|
141
|
+
// fire/replay paths below (resolved once via the wireTruthApply helper so
|
|
142
|
+
// every call site passes the SAME H the gate used). 0 when the flag is OFF
|
|
143
|
+
// (byte-identical to v0.21.11) or no model/window is known.
|
|
144
|
+
const overheadTokens = resolveOverheadTokens({
|
|
145
|
+
config,
|
|
146
|
+
modelId,
|
|
147
|
+
resolvedWindow,
|
|
148
|
+
stateDir: runtime.currentStateDir,
|
|
149
|
+
});
|
|
111
150
|
// 3WF-2: consume a pending live-window delta from a prior compaction. If a
|
|
112
151
|
// compaction fired on the previous context event and the live window did
|
|
113
152
|
// not shrink, this arms the ThrashGuard (meta). No-op when none pending.
|
|
@@ -132,6 +171,18 @@ export function registerContextHandler(
|
|
|
132
171
|
reportedWindow > 0
|
|
133
172
|
? reportedWindow
|
|
134
173
|
: (runtime.currentModel?.contextWindow ?? 0);
|
|
174
|
+
// v0.21.12: calibrate the invisible overhead H from EVERY context event that
|
|
175
|
+
// carries finite usage. estimateTokens is the REAL message-list estimate
|
|
176
|
+
// (engineView is built when wireOverhead is ON), so hSample = the true
|
|
177
|
+
// overhead (system+tools+prepends), not ≈0. Non-fatal; never throws.
|
|
178
|
+
sampleWireOverheadFromUsage({
|
|
179
|
+
runtime,
|
|
180
|
+
config,
|
|
181
|
+
modelId,
|
|
182
|
+
usageTokens: usage?.tokens,
|
|
183
|
+
resolvedWindow,
|
|
184
|
+
estimateTokens,
|
|
185
|
+
});
|
|
135
186
|
runtime.snapshot(ctx);
|
|
136
187
|
if (!config.auto) {
|
|
137
188
|
const tailed = tailResult();
|
|
@@ -145,6 +196,22 @@ export function registerContextHandler(
|
|
|
145
196
|
// message is captured, even if we don't compact this turn. Non-fatal.
|
|
146
197
|
appendMirrorAndLedger(runtime, config, messages);
|
|
147
198
|
|
|
199
|
+
// v0.21.12: WIRED-TRUTH gate override (extracted to wireTruthApply.ts).
|
|
200
|
+
// When the last assistant message is an error whose text matches the
|
|
201
|
+
// provider's 400 shape, the parsed requestTokens become ground-truth
|
|
202
|
+
// currentTokens for THIS event's gate — breaking the 400 loop when the
|
|
203
|
+
// estimate path undercounts by ~50% (the v0.21.11 blind spot). No-op when
|
|
204
|
+
// wireOverhead is OFF (byte-identical).
|
|
205
|
+
currentTokens = applyWireTruthOverride({
|
|
206
|
+
runtime,
|
|
207
|
+
config,
|
|
208
|
+
messages,
|
|
209
|
+
modelId,
|
|
210
|
+
resolvedWindow,
|
|
211
|
+
estimateTokens,
|
|
212
|
+
currentTokens,
|
|
213
|
+
});
|
|
214
|
+
|
|
148
215
|
// S29 FAST GATE: drive the auto-trigger off the context percent (see
|
|
149
216
|
// gateCheck.ts). Returns a tailed view when the gate does not pass.
|
|
150
217
|
const gate = evaluateGate(runtime, config, { pct, currentTokens, tailResult });
|
|
@@ -190,6 +257,7 @@ export function registerContextHandler(
|
|
|
190
257
|
maxOutputTokens: runtime.currentModel?.maxTokens ?? 0,
|
|
191
258
|
outputReservePct: config.outputReservePct,
|
|
192
259
|
safetyMarginPct: runtime.trimCache.safetyMarginPct,
|
|
260
|
+
overheadTokens,
|
|
193
261
|
});
|
|
194
262
|
runtime.diagLiveTrimFires++; // trim view returned this call (replay counts as a fire)
|
|
195
263
|
runtime.diagLiveTrimReplays++;
|
|
@@ -240,6 +308,7 @@ export function registerContextHandler(
|
|
|
240
308
|
pct,
|
|
241
309
|
currentTokens,
|
|
242
310
|
tailResult,
|
|
311
|
+
overheadTokens,
|
|
243
312
|
});
|
|
244
313
|
if (pipeline.kind === "return") return pipeline.view;
|
|
245
314
|
|
|
@@ -321,6 +390,7 @@ export function registerContextHandler(
|
|
|
321
390
|
ran: pipeline.ran,
|
|
322
391
|
perModelThreshold: gate.perModelThreshold,
|
|
323
392
|
tailResult,
|
|
393
|
+
overheadTokens,
|
|
324
394
|
});
|
|
325
395
|
});
|
|
326
396
|
}
|
|
@@ -40,6 +40,8 @@ export class RuntimeInstrumentation {
|
|
|
40
40
|
diagCtxThrown = 0; // live-trim try threw (caught)
|
|
41
41
|
diagCtxOutputErrorTrip = 0; // Phase H: output-error catch tripped a forced compaction
|
|
42
42
|
diagCtxHeadroomTrip = 0; // v0.21.9: output-headroom gate tripped a pre-overflow compaction
|
|
43
|
+
diagCtxWireTruth = 0; // v0.21.12: a provider 400 text was parsed into ground-truth tokens
|
|
44
|
+
diagCtxSkipCapped = 0; // v0.21.12: the live-trim skip path was tail-capped to fit the budget
|
|
43
45
|
|
|
44
46
|
// Context health instrumentation (v0.12): rolling ring buffers for
|
|
45
47
|
// drift detection + cache poison Layer 1 hash baseline.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.12",
|
|
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",
|