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.
@@ -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
- const tierPct = TIER_PCT[tier];
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
- if (currentTokens < runtime.effectiveThreshold) {
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, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
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
- const tierPct = TIER_PCT[tier];
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
- if (currentTokens < runtime.effectiveThreshold) {
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, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
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.83",
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
+ }