pi-mega-compact 0.20.83 → 0.20.84

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.
@@ -167,6 +167,9 @@ export function loadConfig() {
167
167
  recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
168
168
  windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
169
169
  recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
170
+ // 3WF-1: TriggerGuard — guarantee a staged recall block on every context
171
+ // event even when session_start never fires. Default ON; OFF = byte-identical.
172
+ threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
170
173
  // PC-A: positive sprint flag, default ON. =0 byte-identical to the
171
174
  // pre-change OFF state (single gate lives at the call site in tailResult.ts).
172
175
  messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", true),
@@ -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
@@ -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 {};
@@ -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
@@ -203,6 +203,9 @@ export function loadConfig(): MegaConfig {
203
203
  recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
204
204
  windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
205
205
  recallTailInject: envBool("MEGACOMPACT_RECALL_TAIL_INJECT", true),
206
+ // 3WF-1: TriggerGuard — guarantee a staged recall block on every context
207
+ // event even when session_start never fires. Default ON; OFF = byte-identical.
208
+ threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
206
209
  // PC-A: positive sprint flag, default ON. =0 byte-identical to the
207
210
  // pre-change OFF state (single gate lives at the call site in tailResult.ts).
208
211
  messageSeparation: envBool("MEGACOMPACT_MESSAGE_SEPARATION", true),
@@ -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
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.84",
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
+ }