pi-mega-compact 0.20.83 → 0.20.85
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-helpers.js +6 -0
- package/dist/extensions/mega-config.js +23 -1
- package/dist/extensions/mega-events/context-handler/gateCheck.js +14 -2
- package/dist/extensions/mega-events/context-handler/triggerGuard.js +86 -0
- package/dist/extensions/mega-events/context-handler.js +12 -0
- package/dist/extensions/mega-runtime/pressure-getters.js +12 -0
- package/dist/src/failback/types.js +11 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +13 -0
- package/extensions/mega-config-types.ts +4 -0
- package/extensions/mega-config.ts +23 -1
- package/extensions/mega-events/context-handler/gateCheck.ts +18 -2
- package/extensions/mega-events/context-handler/triggerGuard.ts +137 -0
- package/extensions/mega-events/context-handler.ts +11 -0
- package/extensions/mega-runtime/pressure-getters.ts +14 -0
- package/package.json +1 -1
- package/src/failback/types.ts +46 -0
|
@@ -126,6 +126,12 @@ export const SETTINGS = [
|
|
|
126
126
|
num("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", "Embedding Chars per Token", "Estimated characters per token used for embedder chunking size", 4, 1, 32),
|
|
127
127
|
],
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: "Compaction",
|
|
131
|
+
settings: [
|
|
132
|
+
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),
|
|
133
|
+
],
|
|
134
|
+
},
|
|
129
135
|
VECTOR_CORTEX_SETTINGS,
|
|
130
136
|
{
|
|
131
137
|
name: "Cost API",
|
|
@@ -66,11 +66,30 @@ function resolveThreshold() {
|
|
|
66
66
|
}
|
|
67
67
|
const raw = (process.env.MEGACOMPACT_TIER ?? "low").toLowerCase();
|
|
68
68
|
const tier = (raw in COMPACT_TIERS ? raw : "low");
|
|
69
|
-
|
|
69
|
+
let tierPct = TIER_PCT[tier];
|
|
70
|
+
// 3WF-2 threshold invariant: under the umbrella, when no named tier is set
|
|
71
|
+
// the fire point is the configurable % of the ACTUAL model window (default
|
|
72
|
+
// 0.80 — "20% free remaining"). Tiered (named preset) keeps its preset pct;
|
|
73
|
+
// both paths still compute the legacy 200k boot fallback below as a display
|
|
74
|
+
// placeholder + the custom-tier absolute companion. Umbrella OFF stays
|
|
75
|
+
// byte-identical to v0.20.83 (default tier=low 0.5).
|
|
76
|
+
const umbrella = envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true);
|
|
77
|
+
if (umbrella && !(process.env.MEGACOMPACT_TIER && process.env.MEGACOMPACT_TIER !== "")) {
|
|
78
|
+
tierPct = clamp(envFlag("MEGACOMPACT_THRESHOLD_PCT", 0.8), 0.1, 0.95);
|
|
79
|
+
}
|
|
70
80
|
// Boot fallback: sane gate before the first context event provides a window.
|
|
81
|
+
// (NO hardcoded window in the firing path — effectiveThresholdImpl defers
|
|
82
|
+
// when window unknown; this remains only a display placeholder + custom
|
|
83
|
+
// companion under the umbrella.)
|
|
71
84
|
const thresholdTokens = Math.round(tierPct * 200_000);
|
|
72
85
|
return { tier, tierPct, thresholdTokens };
|
|
73
86
|
}
|
|
87
|
+
/** Clamp `n` into [lo, hi]; non-finite → fallback. */
|
|
88
|
+
function clamp(n, lo, hi) {
|
|
89
|
+
if (!Number.isFinite(n))
|
|
90
|
+
return lo;
|
|
91
|
+
return Math.min(hi, Math.max(lo, n));
|
|
92
|
+
}
|
|
74
93
|
/**
|
|
75
94
|
* Pure helper: the real compaction fire point, given the model context window.
|
|
76
95
|
*
|
|
@@ -167,6 +186,9 @@ export function loadConfig() {
|
|
|
167
186
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
|
168
187
|
windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
|
|
169
188
|
recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
|
|
189
|
+
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
190
|
+
// event even when session_start never fires. Default ON; OFF = byte-identical.
|
|
191
|
+
threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
|
|
170
192
|
// PC-A: positive sprint flag, default ON. =0 byte-identical to the
|
|
171
193
|
// pre-change OFF state (single gate lives at the call site in tailResult.ts).
|
|
172
194
|
messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", true),
|
|
@@ -40,11 +40,23 @@ export function evaluateGate(runtime, config, opts) {
|
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
42
42
|
// custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
|
|
43
|
-
|
|
43
|
+
// 3WF-2: under the umbrella, when the window is known AND the config is
|
|
44
|
+
// tiered, honor the per-model Dashboard override exactly like the percent
|
|
45
|
+
// branch (firePointPct % of the actual window) instead of the bare boot
|
|
46
|
+
// fallback. custom / window-unknown / umbrella-OFF keep the bare
|
|
47
|
+
// runtime.effectiveThreshold (window-unknown defers via +Infinity from
|
|
48
|
+
// effectiveThresholdImpl; custom is the explicit absolute).
|
|
49
|
+
let gateThreshold = runtime.effectiveThreshold;
|
|
50
|
+
if (config.threeWayFailback &&
|
|
51
|
+
config.tierPct != null &&
|
|
52
|
+
runtime.lastCtxWindow > 0) {
|
|
53
|
+
gateThreshold = Math.round((perModelThreshold.firePointPct / 100) * runtime.lastCtxWindow);
|
|
54
|
+
}
|
|
55
|
+
if (currentTokens < gateThreshold) {
|
|
44
56
|
runtime.diagCtxFastGate++;
|
|
45
57
|
return { kind: "return", view: tailResult() ?? undefined };
|
|
46
58
|
}
|
|
47
|
-
const check = autoCompactCheck(currentTokens,
|
|
59
|
+
const check = autoCompactCheck(currentTokens, gateThreshold); // SERVER-STYLE CONFIRM (local)
|
|
48
60
|
if (!check.shouldCompact) {
|
|
49
61
|
runtime.diagCtxNoCompact++;
|
|
50
62
|
return { kind: "return", view: tailResult() ?? undefined };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { normalizeSessionId } from "../../../src/store.js";
|
|
2
|
+
import { recall } from "../../../src/engine.js";
|
|
3
|
+
import { formatRecallBlock } from "../../../src/recall.js";
|
|
4
|
+
import { vectorStats, vectorList } from "../../../src/vectorStore.js";
|
|
5
|
+
import { recentUserQuery } from "../../mega-runtime.js";
|
|
6
|
+
/** One-shot completion marker per MegaRuntime (dies with the runtime). */
|
|
7
|
+
const guardDone = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* Run the TriggerGuard for this context event. Best-effort: never throws; any
|
|
10
|
+
* failure degrades to the pre-sprint path (no staged block).
|
|
11
|
+
*/
|
|
12
|
+
export function runTriggerGuard(runtime, config, ctx) {
|
|
13
|
+
try {
|
|
14
|
+
// Flag OFF => byte-identical pre-sprint behavior (no touch, no telemetry).
|
|
15
|
+
if (!config.threeWayFailback)
|
|
16
|
+
return;
|
|
17
|
+
// session_start already staged a block => the normal path wins, no-op.
|
|
18
|
+
if (runtime.pendingRecallBlock != null)
|
|
19
|
+
return;
|
|
20
|
+
// One-shot: we've already decided for this runtime.
|
|
21
|
+
if (guardDone.has(runtime))
|
|
22
|
+
return;
|
|
23
|
+
// recentUserQuery only reads sessionManager.getEntries(); the structural
|
|
24
|
+
// GuardCtx satisfies it at runtime (cast to the full type it expects).
|
|
25
|
+
const query = recentUserQuery(ctx);
|
|
26
|
+
if (!query) {
|
|
27
|
+
guardDone.set(runtime, { done: true });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
|
|
31
|
+
const stats = vectorStats(runtime.store, sid);
|
|
32
|
+
// Empty store => genuinely new session; not a recall failure. Mark done so
|
|
33
|
+
// we don't re-check forever, stage nothing (no crash, no floor).
|
|
34
|
+
if (stats.checkpointCount <= 0) {
|
|
35
|
+
guardDone.set(runtime, { done: true });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Read-only recall: search + rank only; skipInjected:false means we keep
|
|
39
|
+
// hits already injected this session, but we never call vectorMarkInjected
|
|
40
|
+
// ourselves (A1) — formatRecallBlock does the same as session_start uses.
|
|
41
|
+
const hits = recall({ sessionId: sid, query, limit: config.autoInlineK, skipInjected: false }, runtime.store).hits;
|
|
42
|
+
if (hits.length > 0) {
|
|
43
|
+
runtime.pendingRecallBlock = formatRecallBlock(hits);
|
|
44
|
+
guardDone.set(runtime, { done: true });
|
|
45
|
+
runtime.appendEvent("three_way_guard_fired", {
|
|
46
|
+
source: "recall",
|
|
47
|
+
hitCount: hits.length,
|
|
48
|
+
topScore: hits[0]?.score ?? null,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Store HAS checkpoints but recall found nothing relevant: stage a
|
|
53
|
+
// provenance floor built from the newest checkpoint summary rather than
|
|
54
|
+
// silence (the incident's "recall silently never ran" shape).
|
|
55
|
+
const floor = buildFloorBlock(runtime, sid);
|
|
56
|
+
runtime.pendingRecallBlock = floor;
|
|
57
|
+
guardDone.set(runtime, { done: true });
|
|
58
|
+
runtime.appendEvent("three_way_floor_used", { basis: "lastCheckpoint" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* never throws; best-effort guard */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Build the provenance floor string from the session's newest checkpoint. */
|
|
65
|
+
function buildFloorBlock(runtime, sid) {
|
|
66
|
+
try {
|
|
67
|
+
const cps = vectorList(runtime.store, sid);
|
|
68
|
+
let newest = cps[0];
|
|
69
|
+
for (const cp of cps) {
|
|
70
|
+
if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0))
|
|
71
|
+
newest = cp;
|
|
72
|
+
}
|
|
73
|
+
const summary = newest?.summary?.trim();
|
|
74
|
+
if (summary) {
|
|
75
|
+
return ("The following compacted context is the most recent checkpoint from " +
|
|
76
|
+
"this session (recall found no query-relevant match):\n\n" + summary);
|
|
77
|
+
}
|
|
78
|
+
return ("This session has compacted context but recall could not surface a " +
|
|
79
|
+
"checkpoint relevant to the current request; the most recent checkpoint " +
|
|
80
|
+
"summary is unavailable.");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return ("This session has compacted context but recall could not surface a " +
|
|
84
|
+
"checkpoint relevant to the current request.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { estimateSessionTokens } from "../../src/tokens.js";
|
|
2
2
|
import { piCompactWouldNoop } from "../mega-pipeline.js";
|
|
3
3
|
import { buildTailResult } from "./context-handler/tailResult.js";
|
|
4
|
+
import { runTriggerGuard } from "./context-handler/triggerGuard.js";
|
|
4
5
|
import { persistEpochAndMaintain } from "./context-handler/afterCompact.js";
|
|
5
6
|
import { appendMirrorAndLedger } from "./context-handler/dbMirrorAppend.js";
|
|
6
7
|
import { evaluateGate } from "./context-handler/gateCheck.js";
|
|
@@ -26,6 +27,17 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
26
27
|
const usage = ctx.getContextUsage();
|
|
27
28
|
const pct = usage?.percent;
|
|
28
29
|
const messages = event.messages;
|
|
30
|
+
// 3WF-1 TriggerGuard: guarantee a staged recall block on this event even
|
|
31
|
+
// when session_start never fired. One-shot per session; a block already
|
|
32
|
+
// staged by session_start takes precedence (no-op). Run BEFORE building the
|
|
33
|
+
// tail factory so a freshly staged block is composed into THIS event's view.
|
|
34
|
+
// Best-effort — a failure falls through to the pre-sprint path.
|
|
35
|
+
try {
|
|
36
|
+
runTriggerGuard(runtime, config, ctx);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* non-fatal */
|
|
40
|
+
}
|
|
29
41
|
// S53: helper to inject the staged recall/memory block as a user-role
|
|
30
42
|
// tail message at any view-return point. Returns undefined when nothing
|
|
31
43
|
// is staged (or the flag is OFF) so the caller falls through to its
|
|
@@ -51,6 +51,18 @@ export function pressureImpl(self) {
|
|
|
51
51
|
* always below pi's native auto-compaction (~80% of window).
|
|
52
52
|
*/
|
|
53
53
|
export function effectiveThresholdImpl(self) {
|
|
54
|
+
// 3WF-2 threshold invariant: under the umbrella, a tiered config with an
|
|
55
|
+
// UNKNOWN window (lastCtxWindow <= 0) DEFERS — auto-compaction must never
|
|
56
|
+
// substitute a guessed window. Returning +Infinity keeps every downstream
|
|
57
|
+
// `tokens >= threshold` comparison false (gateCheck token path,
|
|
58
|
+
// agent_end durable trigger, live-trim re-compact), so no compaction fires
|
|
59
|
+
// until the provider reports a real window. custom (tierPct null) and
|
|
60
|
+
// umbrella-OFF fall through to the legacy helper (byte-identical).
|
|
61
|
+
if (self.config.threeWayFailback &&
|
|
62
|
+
self.config.tierPct != null &&
|
|
63
|
+
self.lastCtxWindow <= 0) {
|
|
64
|
+
return Number.POSITIVE_INFINITY;
|
|
65
|
+
}
|
|
54
66
|
return effectiveThresholdTokens({
|
|
55
67
|
tierPct: self.config.tierPct,
|
|
56
68
|
fallbackThreshold: self.config.thresholdTokens,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/failback/types.ts — 3WF Three-Way Failback contract types (contract-first).
|
|
3
|
+
*
|
|
4
|
+
* Pure, pi-agnostic interfaces describing the staged-recall guarantee chain:
|
|
5
|
+
* every session should have a staged recall block even when `session_start`
|
|
6
|
+
* never fires. 3WF-1 (TriggerGuard) is the first consumer; 3WF-2/3/4 extend
|
|
7
|
+
* these shapes for the compaction ladder, the 3-source recall vote, and
|
|
8
|
+
* InjectionConfirm. No pi runtime imports, no store mutation — these are the
|
|
9
|
+
* shape contract only.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -274,6 +274,19 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
|
|
|
274
274
|
num("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", "Embedding Chars per Token", "Estimated characters per token used for embedder chunking size", 4, 1, 32),
|
|
275
275
|
],
|
|
276
276
|
},
|
|
277
|
+
{
|
|
278
|
+
name: "Compaction",
|
|
279
|
+
settings: [
|
|
280
|
+
num(
|
|
281
|
+
"MEGACOMPACT_THRESHOLD_PCT",
|
|
282
|
+
"Compaction Threshold",
|
|
283
|
+
"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",
|
|
284
|
+
0.8,
|
|
285
|
+
0.1,
|
|
286
|
+
0.95,
|
|
287
|
+
),
|
|
288
|
+
],
|
|
289
|
+
},
|
|
277
290
|
VECTOR_CORTEX_SETTINGS,
|
|
278
291
|
{
|
|
279
292
|
name: "Cost API",
|
|
@@ -136,6 +136,10 @@ export interface MegaConfig {
|
|
|
136
136
|
* the tail of the view when auto is OFF AND no trim action is needed. Default ON
|
|
137
137
|
* (true). When false, restores the pre-sprint systemPrompt prepend behavior. */
|
|
138
138
|
recallTailInject: boolean;
|
|
139
|
+
/** 3WF-1: TriggerGuard — re-stage a recall block at the context event seam when
|
|
140
|
+
* session_start never fired, so every session has a staged block (recall hits,
|
|
141
|
+
* else a provenance floor). Default ON; OFF = byte-identical pre-sprint. */
|
|
142
|
+
threeWayFailback: boolean;
|
|
139
143
|
/** A1 PLAN_V2 Phase 2: Message Separation — isolate user/assistant turns
|
|
140
144
|
* from volatile tool results so the prompt-cache prefix stays stable.
|
|
141
145
|
* PC-A: positive sprint flag, now default ON; flag-OFF (=0) is byte-identical
|
|
@@ -78,12 +78,31 @@ function resolveThreshold(): {
|
|
|
78
78
|
}
|
|
79
79
|
const raw = (process.env.MEGACOMPACT_TIER ?? "low").toLowerCase();
|
|
80
80
|
const tier = (raw in COMPACT_TIERS ? raw : "low") as CompactTier;
|
|
81
|
-
|
|
81
|
+
let tierPct = TIER_PCT[tier];
|
|
82
|
+
// 3WF-2 threshold invariant: under the umbrella, when no named tier is set
|
|
83
|
+
// the fire point is the configurable % of the ACTUAL model window (default
|
|
84
|
+
// 0.80 — "20% free remaining"). Tiered (named preset) keeps its preset pct;
|
|
85
|
+
// both paths still compute the legacy 200k boot fallback below as a display
|
|
86
|
+
// placeholder + the custom-tier absolute companion. Umbrella OFF stays
|
|
87
|
+
// byte-identical to v0.20.83 (default tier=low 0.5).
|
|
88
|
+
const umbrella = envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true);
|
|
89
|
+
if (umbrella && !(process.env.MEGACOMPACT_TIER && process.env.MEGACOMPACT_TIER !== "")) {
|
|
90
|
+
tierPct = clamp(envFlag("MEGACOMPACT_THRESHOLD_PCT", 0.8), 0.1, 0.95);
|
|
91
|
+
}
|
|
82
92
|
// Boot fallback: sane gate before the first context event provides a window.
|
|
93
|
+
// (NO hardcoded window in the firing path — effectiveThresholdImpl defers
|
|
94
|
+
// when window unknown; this remains only a display placeholder + custom
|
|
95
|
+
// companion under the umbrella.)
|
|
83
96
|
const thresholdTokens = Math.round(tierPct * 200_000);
|
|
84
97
|
return { tier, tierPct, thresholdTokens };
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
/** Clamp `n` into [lo, hi]; non-finite → fallback. */
|
|
101
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
102
|
+
if (!Number.isFinite(n)) return lo;
|
|
103
|
+
return Math.min(hi, Math.max(lo, n));
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/**
|
|
88
107
|
* Pure helper: the real compaction fire point, given the model context window.
|
|
89
108
|
*
|
|
@@ -203,6 +222,9 @@ export function loadConfig(): MegaConfig {
|
|
|
203
222
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
|
204
223
|
windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
|
|
205
224
|
recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
|
|
225
|
+
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
226
|
+
// event even when session_start never fires. Default ON; OFF = byte-identical.
|
|
227
|
+
threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
|
|
206
228
|
// PC-A: positive sprint flag, default ON. =0 byte-identical to the
|
|
207
229
|
// pre-change OFF state (single gate lives at the call site in tailResult.ts).
|
|
208
230
|
messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", true),
|
|
@@ -82,11 +82,27 @@ export function evaluateGate(
|
|
|
82
82
|
gatePassed = pct / 100 >= firePct;
|
|
83
83
|
} else {
|
|
84
84
|
// custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
|
|
85
|
-
|
|
85
|
+
// 3WF-2: under the umbrella, when the window is known AND the config is
|
|
86
|
+
// tiered, honor the per-model Dashboard override exactly like the percent
|
|
87
|
+
// branch (firePointPct % of the actual window) instead of the bare boot
|
|
88
|
+
// fallback. custom / window-unknown / umbrella-OFF keep the bare
|
|
89
|
+
// runtime.effectiveThreshold (window-unknown defers via +Infinity from
|
|
90
|
+
// effectiveThresholdImpl; custom is the explicit absolute).
|
|
91
|
+
let gateThreshold = runtime.effectiveThreshold;
|
|
92
|
+
if (
|
|
93
|
+
config.threeWayFailback &&
|
|
94
|
+
config.tierPct != null &&
|
|
95
|
+
runtime.lastCtxWindow > 0
|
|
96
|
+
) {
|
|
97
|
+
gateThreshold = Math.round(
|
|
98
|
+
(perModelThreshold.firePointPct / 100) * runtime.lastCtxWindow,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (currentTokens < gateThreshold) {
|
|
86
102
|
runtime.diagCtxFastGate++;
|
|
87
103
|
return { kind: "return", view: tailResult() ?? undefined };
|
|
88
104
|
}
|
|
89
|
-
const check = autoCompactCheck(currentTokens,
|
|
105
|
+
const check = autoCompactCheck(currentTokens, gateThreshold); // SERVER-STYLE CONFIRM (local)
|
|
90
106
|
if (!check.shouldCompact) {
|
|
91
107
|
runtime.diagCtxNoCompact++;
|
|
92
108
|
return { kind: "return", view: tailResult() ?? undefined };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/mega-events/context-handler/triggerGuard.ts — 3WF-1 TriggerGuard.
|
|
3
|
+
*
|
|
4
|
+
* Production-incident fix: `session_start` can silently never fire (pi host
|
|
5
|
+
* behavior), so the only recall staging point (session-handlers.ts) is skipped
|
|
6
|
+
* and a session replays with NO recall block and NO telemetry. TriggerGuard
|
|
7
|
+
* re-stages at the `context` event seam — the one place that ALWAYS runs and
|
|
8
|
+
* where the staged block is composed into the model view on this same event.
|
|
9
|
+
*
|
|
10
|
+
* Stack position (context-handler.ts): run DIRECTLY BEFORE `buildTailResult` is
|
|
11
|
+
* built, so a freshly staged block is picked up by the existing tail machinery
|
|
12
|
+
* (recall-tail.ts) with zero changes to it.
|
|
13
|
+
*
|
|
14
|
+
* Contract:
|
|
15
|
+
* - one-shot per MegaRuntime (WeakMap) — recall does NOT re-run on every event.
|
|
16
|
+
* - if runtime.pendingRecallBlock != null already, no-op (session_start won).
|
|
17
|
+
* - read-only recall via engine.recall -> formatRecallBlock; no vectorMarkInjected,
|
|
18
|
+
* no S43 telemetry, no turn writes (this is a guard, not an inline).
|
|
19
|
+
* - an empty store (checkpointCount == 0) is a genuinely new session: no crash,
|
|
20
|
+
* no floor staged, just marked done.
|
|
21
|
+
* - a store WITH checkpoints whose recall returns nothing: stage the provenance
|
|
22
|
+
* floor (newest checkpoint summary) instead of silence.
|
|
23
|
+
* - never throws (whole body guarded), non-fatal, PREVENT-PI-003 (block is plain
|
|
24
|
+
* text; role decided downstream by withRecallTail as user).
|
|
25
|
+
*/
|
|
26
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import type { MegaRuntime } from "../../mega-runtime.js";
|
|
28
|
+
import type { MegaConfig } from "../../mega-config.js";
|
|
29
|
+
import { normalizeSessionId } from "../../../src/store.js";
|
|
30
|
+
import { recall } from "../../../src/engine.js";
|
|
31
|
+
import { formatRecallBlock } from "../../../src/recall.js";
|
|
32
|
+
import { vectorStats, vectorList } from "../../../src/vectorStore.js";
|
|
33
|
+
import { recentUserQuery } from "../../mega-runtime.js";
|
|
34
|
+
|
|
35
|
+
/** One-shot completion marker per MegaRuntime (dies with the runtime). */
|
|
36
|
+
const guardDone = new WeakMap<MegaRuntime, { done: true }>();
|
|
37
|
+
|
|
38
|
+
/** The minimal ctx surface the guard reads (query + session id). Accepted instead
|
|
39
|
+
* of the full ExtensionContext so tests can pass a thin stub without a full
|
|
40
|
+
* pi ctx; structurally satisfied by the real ExtensionContext. */
|
|
41
|
+
interface GuardCtx {
|
|
42
|
+
sessionManager: { getSessionId(): string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run the TriggerGuard for this context event. Best-effort: never throws; any
|
|
48
|
+
* failure degrades to the pre-sprint path (no staged block).
|
|
49
|
+
*/
|
|
50
|
+
export function runTriggerGuard(
|
|
51
|
+
runtime: MegaRuntime,
|
|
52
|
+
config: MegaConfig,
|
|
53
|
+
ctx: GuardCtx,
|
|
54
|
+
): void {
|
|
55
|
+
try {
|
|
56
|
+
// Flag OFF => byte-identical pre-sprint behavior (no touch, no telemetry).
|
|
57
|
+
if (!config.threeWayFailback) return;
|
|
58
|
+
// session_start already staged a block => the normal path wins, no-op.
|
|
59
|
+
if (runtime.pendingRecallBlock != null) return;
|
|
60
|
+
// One-shot: we've already decided for this runtime.
|
|
61
|
+
if (guardDone.has(runtime)) return;
|
|
62
|
+
|
|
63
|
+
// recentUserQuery only reads sessionManager.getEntries(); the structural
|
|
64
|
+
// GuardCtx satisfies it at runtime (cast to the full type it expects).
|
|
65
|
+
const query = recentUserQuery(ctx as unknown as ExtensionContext);
|
|
66
|
+
if (!query) {
|
|
67
|
+
guardDone.set(runtime, { done: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
|
|
72
|
+
const stats = vectorStats(runtime.store, sid);
|
|
73
|
+
// Empty store => genuinely new session; not a recall failure. Mark done so
|
|
74
|
+
// we don't re-check forever, stage nothing (no crash, no floor).
|
|
75
|
+
if (stats.checkpointCount <= 0) {
|
|
76
|
+
guardDone.set(runtime, { done: true });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Read-only recall: search + rank only; skipInjected:false means we keep
|
|
81
|
+
// hits already injected this session, but we never call vectorMarkInjected
|
|
82
|
+
// ourselves (A1) — formatRecallBlock does the same as session_start uses.
|
|
83
|
+
const hits = recall(
|
|
84
|
+
{ sessionId: sid, query, limit: config.autoInlineK, skipInjected: false },
|
|
85
|
+
runtime.store,
|
|
86
|
+
).hits;
|
|
87
|
+
|
|
88
|
+
if (hits.length > 0) {
|
|
89
|
+
runtime.pendingRecallBlock = formatRecallBlock(hits);
|
|
90
|
+
guardDone.set(runtime, { done: true });
|
|
91
|
+
runtime.appendEvent("three_way_guard_fired", {
|
|
92
|
+
source: "recall",
|
|
93
|
+
hitCount: hits.length,
|
|
94
|
+
topScore: hits[0]?.score ?? null,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Store HAS checkpoints but recall found nothing relevant: stage a
|
|
100
|
+
// provenance floor built from the newest checkpoint summary rather than
|
|
101
|
+
// silence (the incident's "recall silently never ran" shape).
|
|
102
|
+
const floor = buildFloorBlock(runtime, sid);
|
|
103
|
+
runtime.pendingRecallBlock = floor;
|
|
104
|
+
guardDone.set(runtime, { done: true });
|
|
105
|
+
runtime.appendEvent("three_way_floor_used", { basis: "lastCheckpoint" });
|
|
106
|
+
} catch {
|
|
107
|
+
/* never throws; best-effort guard */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build the provenance floor string from the session's newest checkpoint. */
|
|
112
|
+
function buildFloorBlock(runtime: MegaRuntime, sid: string): string {
|
|
113
|
+
try {
|
|
114
|
+
const cps = vectorList(runtime.store, sid);
|
|
115
|
+
let newest = cps[0];
|
|
116
|
+
for (const cp of cps) {
|
|
117
|
+
if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0)) newest = cp;
|
|
118
|
+
}
|
|
119
|
+
const summary = newest?.summary?.trim();
|
|
120
|
+
if (summary) {
|
|
121
|
+
return (
|
|
122
|
+
"The following compacted context is the most recent checkpoint from " +
|
|
123
|
+
"this session (recall found no query-relevant match):\n\n" + summary
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return (
|
|
127
|
+
"This session has compacted context but recall could not surface a " +
|
|
128
|
+
"checkpoint relevant to the current request; the most recent checkpoint " +
|
|
129
|
+
"summary is unavailable."
|
|
130
|
+
);
|
|
131
|
+
} catch {
|
|
132
|
+
return (
|
|
133
|
+
"This session has compacted context but recall could not surface a " +
|
|
134
|
+
"checkpoint relevant to the current request."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -23,6 +23,7 @@ import type { MegaRuntime } from "../mega-runtime.js";
|
|
|
23
23
|
import { piCompactWouldNoop } from "../mega-pipeline.js";
|
|
24
24
|
import type { MegaConfig } from "../mega-config.js";
|
|
25
25
|
import { buildTailResult } from "./context-handler/tailResult.js";
|
|
26
|
+
import { runTriggerGuard } from "./context-handler/triggerGuard.js";
|
|
26
27
|
import { persistEpochAndMaintain } from "./context-handler/afterCompact.js";
|
|
27
28
|
import { appendMirrorAndLedger } from "./context-handler/dbMirrorAppend.js";
|
|
28
29
|
import { evaluateGate } from "./context-handler/gateCheck.js";
|
|
@@ -53,6 +54,16 @@ export function registerContextHandler(
|
|
|
53
54
|
const usage = ctx.getContextUsage();
|
|
54
55
|
const pct = usage?.percent;
|
|
55
56
|
const messages = event.messages;
|
|
57
|
+
// 3WF-1 TriggerGuard: guarantee a staged recall block on this event even
|
|
58
|
+
// when session_start never fired. One-shot per session; a block already
|
|
59
|
+
// staged by session_start takes precedence (no-op). Run BEFORE building the
|
|
60
|
+
// tail factory so a freshly staged block is composed into THIS event's view.
|
|
61
|
+
// Best-effort — a failure falls through to the pre-sprint path.
|
|
62
|
+
try {
|
|
63
|
+
runTriggerGuard(runtime, config, ctx);
|
|
64
|
+
} catch {
|
|
65
|
+
/* non-fatal */
|
|
66
|
+
}
|
|
56
67
|
// S53: helper to inject the staged recall/memory block as a user-role
|
|
57
68
|
// tail message at any view-return point. Returns undefined when nothing
|
|
58
69
|
// is staged (or the flag is OFF) so the caller falls through to its
|
|
@@ -81,6 +81,20 @@ export function pressureImpl(self: PressureContext): number {
|
|
|
81
81
|
* always below pi's native auto-compaction (~80% of window).
|
|
82
82
|
*/
|
|
83
83
|
export function effectiveThresholdImpl(self: PressureContext): number {
|
|
84
|
+
// 3WF-2 threshold invariant: under the umbrella, a tiered config with an
|
|
85
|
+
// UNKNOWN window (lastCtxWindow <= 0) DEFERS — auto-compaction must never
|
|
86
|
+
// substitute a guessed window. Returning +Infinity keeps every downstream
|
|
87
|
+
// `tokens >= threshold` comparison false (gateCheck token path,
|
|
88
|
+
// agent_end durable trigger, live-trim re-compact), so no compaction fires
|
|
89
|
+
// until the provider reports a real window. custom (tierPct null) and
|
|
90
|
+
// umbrella-OFF fall through to the legacy helper (byte-identical).
|
|
91
|
+
if (
|
|
92
|
+
self.config.threeWayFailback &&
|
|
93
|
+
self.config.tierPct != null &&
|
|
94
|
+
self.lastCtxWindow <= 0
|
|
95
|
+
) {
|
|
96
|
+
return Number.POSITIVE_INFINITY;
|
|
97
|
+
}
|
|
84
98
|
return effectiveThresholdTokens({
|
|
85
99
|
tierPct: self.config.tierPct,
|
|
86
100
|
fallbackThreshold: self.config.thresholdTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.85",
|
|
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",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/failback/types.ts — 3WF Three-Way Failback contract types (contract-first).
|
|
3
|
+
*
|
|
4
|
+
* Pure, pi-agnostic interfaces describing the staged-recall guarantee chain:
|
|
5
|
+
* every session should have a staged recall block even when `session_start`
|
|
6
|
+
* never fires. 3WF-1 (TriggerGuard) is the first consumer; 3WF-2/3/4 extend
|
|
7
|
+
* these shapes for the compaction ladder, the 3-source recall vote, and
|
|
8
|
+
* InjectionConfirm. No pi runtime imports, no store mutation — these are the
|
|
9
|
+
* shape contract only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One-shot guard state for a session (observable result of a TriggerGuard run). */
|
|
13
|
+
export interface TriggerGuardState {
|
|
14
|
+
/** True once a recall/floor attempt has run for this session. */
|
|
15
|
+
recallRan: boolean;
|
|
16
|
+
/** The block staged into runtime.pendingRecallBlock (null when nothing staged). */
|
|
17
|
+
stagedBlock: string | null;
|
|
18
|
+
/** True when the provenance floor (not a recall hit) was staged. */
|
|
19
|
+
usedFloor: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A provenance floor string built from the newest checkpoint / session basis. */
|
|
23
|
+
export interface FloorBlock {
|
|
24
|
+
/** The model-visible floor text. */
|
|
25
|
+
text: string;
|
|
26
|
+
/** Why this floor was produced. */
|
|
27
|
+
basis: "lastCheckpoint" | "sessionProvenance" | "none";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Result of a single TriggerGuard evaluation (for tests + telemetry). */
|
|
31
|
+
export interface GuardRunResult {
|
|
32
|
+
/** How the block (or non-block) was produced. */
|
|
33
|
+
source: "already-staged" | "recall" | "floor" | "none";
|
|
34
|
+
/** The staged block text; null when nothing was staged. */
|
|
35
|
+
block: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Options the TriggerGuard needs to run a staged recall. */
|
|
39
|
+
export interface GuardOpts {
|
|
40
|
+
/** The latest user message used as the recall query; empty => 'none'. */
|
|
41
|
+
query: string | null;
|
|
42
|
+
/** Normalized session id. */
|
|
43
|
+
sessionId: string;
|
|
44
|
+
/** Max hits to recall (mirrors autoInlineK). */
|
|
45
|
+
limit: number;
|
|
46
|
+
}
|