pi-mega-compact 0.20.87 → 0.21.0

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.
@@ -134,6 +134,13 @@ export const SETTINGS = [
134
134
  num("MEGACOMPACT_THRASH_REARM_PCT", "Thrash Re-arm %", "After an ineffective compaction (live window did not shrink), refuse to re-fire until the live window grows by this fraction of the effective threshold. Default 0.10 (10%)", 0.1, 0.01, 0.5),
135
135
  ],
136
136
  },
137
+ {
138
+ name: "Three-Way Failback",
139
+ settings: [
140
+ boolDirect("MEGACOMPACT_THREE_WAY_FAILBACK", "Three-Way Failback", "Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
141
+ boolDirect("MEGACOMPACT_RECALL_TAIL_INJECT", "Recall Tail Inject", "Compose the staged recall block as a trailing user message on the context event (tail inject) instead of the legacy system-prompt prepend. Tail mode keeps the cache prefix stable and is the mode InjectionConfirm verifies against ContextEvent.messages; OFF falls back to the legacy prepend path (verified by string-contains). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
142
+ ],
143
+ },
137
144
  VECTOR_CORTEX_SETTINGS,
138
145
  {
139
146
  name: "Cost API",
@@ -0,0 +1,63 @@
1
+ /**
2
+ * context-handler/injectionConfirm.fixture.ts — shared fixtures for the 3WF-4
3
+ * InjectionConfirm tests.
4
+ *
5
+ * Split out so each test file stays under the extensions/300-soft-cap the way
6
+ * src/recall/recall3wf.fixture.ts does for 3WF-3. These are REAL fixtures, not
7
+ * mocks/stubs: a REAL VectorStore over a temp stateDir with REAL checkpoints
8
+ * persisted via compactSession; the MegaRuntime is a minimal typed stub exposing
9
+ * only the fields confirmInjection touches (store, pendingRecallBlock,
10
+ * pendingMemoryRecallBlock, appendEvent), matching the triggerGuard/thrashGuard
11
+ * test conventions.
12
+ */
13
+ import { mkdtempSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { VectorStore } from "../../../src/vectorStore.js";
17
+ import { compactSession } from "../../../src/engine.js";
18
+ /** Real EngineMessage fixture. */
19
+ export function msg(role, text) {
20
+ return { role, text };
21
+ }
22
+ /** A user-role AgentMessage carrying `text` (the tail-block shape). */
23
+ export function userMsg(text) {
24
+ return { role: "user", content: text, timestamp: 1 };
25
+ }
26
+ /** Fresh isolated state dir per VectorStore. */
27
+ export function freshStore() {
28
+ const dir = mkdtempSync(join(tmpdir(), "mc-inject-"));
29
+ return { store: new VectorStore({ dedupSim: 0.9, stateDir: dir }), dir };
30
+ }
31
+ /** Persist N distinct checkpoints with ascending timestamps. */
32
+ export function seed(store, topics, sid = "sess_inject") {
33
+ topics.forEach((t, i) => {
34
+ compactSession({
35
+ sessionId: sid,
36
+ messages: [msg("user", t), msg("assistant", "ok")],
37
+ keepFrom: 2,
38
+ timestamp: i + 1,
39
+ }, store);
40
+ });
41
+ }
42
+ /** Minimal MegaRuntime stub exposing only the confirmInjection touch-points. */
43
+ export function runtimeStub(store, over = {}) {
44
+ const events = [];
45
+ const runtime = {
46
+ store,
47
+ pendingRecallBlock: over.pendingRecallBlock,
48
+ pendingMemoryRecallBlock: over.pendingMemoryRecallBlock,
49
+ perfTurnStart: undefined,
50
+ rt: { recallInjectedThisTurn: false },
51
+ appendEvent: (name, payload) => {
52
+ events.push({ name, payload });
53
+ },
54
+ };
55
+ return { runtime, events };
56
+ }
57
+ /** Config stub: only the flags confirmInjection reads. */
58
+ export function configStub(over = {}) {
59
+ return {
60
+ threeWayFailback: over.threeWayFailback ?? true,
61
+ recallTailInject: over.recallTailInject ?? true,
62
+ };
63
+ }
@@ -0,0 +1,107 @@
1
+ import { vectorList } from "../../../src/vectorStore.js";
2
+ import { normalizeSessionId } from "../../../src/store.js";
3
+ import { buildFloorBlock } from "../../../src/failback/floor.js";
4
+ import { withRecallTail } from "../recall-tail.js";
5
+ import { messageContentText } from "./messageText.js";
6
+ /**
7
+ * The marker substring used to locate a staged block inside a message. The
8
+ * block's first non-empty line, capped, so prompt reshapes (cache striping /
9
+ * message separation) that regroup messages cannot defeat the match, while a
10
+ * genuinely dropped block still fails it.
11
+ */
12
+ export function blockMarker(block) {
13
+ const line = block
14
+ .split("\n")
15
+ .map((l) => l.trim())
16
+ .find((l) => l.length > 0);
17
+ return (line ?? "").slice(0, 80);
18
+ }
19
+ /**
20
+ * PURE decision function: did the staged block land in this view, and if not,
21
+ * which rung should repair it? Takes the already-extracted message texts so it
22
+ * stays free of pi types and is directly unit-testable.
23
+ */
24
+ export function decideInjection(input, messageTexts, hasPendingBlocks) {
25
+ const marker = input.staged ? blockMarker(input.staged) : "";
26
+ // Nothing staged => nothing to assert; NOT a miss. Injecting a floor here
27
+ // would push provenance text into sessions that never had recall to lose.
28
+ if (!marker)
29
+ return { landed: true, recovered: "none" };
30
+ const landed = messageTexts.some((t) => t.includes(marker));
31
+ if (landed)
32
+ return { landed: true, recovered: "none" };
33
+ // Absent: recompose when the runtime still holds pending blocks, else floor.
34
+ return { landed: false, recovered: hasPendingBlocks ? "recomposed" : "floor" };
35
+ }
36
+ /** Append `text` as a user-role tail message (same shape as recall-tail.ts). */
37
+ function withFloorTail(view, text) {
38
+ const tailMsg = {
39
+ role: "user",
40
+ content: text,
41
+ timestamp: Date.now(),
42
+ };
43
+ return { messages: [...view.messages, tailMsg] };
44
+ }
45
+ /**
46
+ * Thin caller: verify (and if needed repair) one composed view. Returns the view
47
+ * to actually return from the handler. `sessionId` sources the floor checkpoints.
48
+ *
49
+ * The recompose rung deliberately re-appends via `withRecallTail` onto the
50
+ * ALREADY-COMPOSED view rather than re-running `buildTailResult`: the reshape
51
+ * stages (cache striping / message separation) are the realistic way a tail
52
+ * message gets regrouped away, and re-running the same composition would
53
+ * reproduce the same loss. Appending after the reshape is the actual repair, and
54
+ * it keeps the PREVENT-PI-001/002 tail-append invariant (a single user-role
55
+ * message after a complete prefix can never split a toolCall/toolResult pair).
56
+ */
57
+ export function confirmInjection(runtime, config, view, sessionId) {
58
+ try {
59
+ if (!config.threeWayFailback)
60
+ return view;
61
+ // What the tail composition was supposed to inject. BOTH staged blocks
62
+ // count: withRecallTail joins recall + memory blocks into one tail
63
+ // message, so either one going missing is a real injection failure.
64
+ const staged = runtime.pendingRecallBlock ?? runtime.pendingMemoryRecallBlock ?? null;
65
+ // Can the recompose rung actually re-append? Only when a block is still
66
+ // staged on the runtime. When it is not (blocks consumed between
67
+ // composition and this check), or when withRecallTail declines to append,
68
+ // the ladder falls through to the floor rung below.
69
+ const hasPending = runtime.pendingRecallBlock != null ||
70
+ runtime.pendingMemoryRecallBlock != null;
71
+ // Legacy prepend mode: the block is not expected in the message list —
72
+ // verify our composed return value contains it instead (A3 degrade path).
73
+ if (!config.recallTailInject) {
74
+ const composed = view.messages.map(messageContentText).join("\n");
75
+ const marker = staged ? blockMarker(staged) : "";
76
+ runtime.appendEvent("injection_confirmed", {
77
+ mode: "prepend",
78
+ landed: marker ? composed.includes(marker) : true,
79
+ });
80
+ return view;
81
+ }
82
+ const verdict = decideInjection({ staged, tailMode: true }, view.messages.map(messageContentText), hasPending);
83
+ if (verdict.landed) {
84
+ runtime.appendEvent("injection_confirmed", { mode: "tail", landed: true });
85
+ return view;
86
+ }
87
+ if (verdict.recovered === "recomposed") {
88
+ const rebuilt = withRecallTail(view.messages, runtime, config);
89
+ // withRecallTail returns the input array unchanged on failure; only
90
+ // treat a genuine append as a recovery.
91
+ if (rebuilt.length > view.messages.length) {
92
+ runtime.appendEvent("injection_recovered", { via: "recomposed" });
93
+ return { messages: rebuilt };
94
+ }
95
+ }
96
+ const floor = buildFloorBlock(vectorList(runtime.store, normalizeSessionId(sessionId)));
97
+ runtime.appendEvent("injection_recovered", {
98
+ via: "floor",
99
+ basis: floor.basis,
100
+ });
101
+ return withFloorTail(view, floor.text);
102
+ }
103
+ catch {
104
+ // Non-fatal: return the unverified view (pre-sprint behavior).
105
+ return view;
106
+ }
107
+ }
@@ -48,8 +48,17 @@ export const ReductionValidator = {
48
48
  * Infinity/NaN into meta (getMetaNumber would read it back as 0). Skip arming
49
49
  * + log instead; the next over-threshold event simply re-fires (pre-sprint
50
50
  * behavior) rather than corrupting the guard.
51
+ *
52
+ * 3WF-5 telemetry: `logger` is the debug-gated runtime logger (mega-compact.log,
53
+ * silent unless config.debug) — which means the breadcrumb was invisible to the
54
+ * dashboard Events tab, whose SSE tail reads the repo's events.log. `emit` is
55
+ * the always-on events.log sink (MegaRuntime.appendEvent), so the armed
56
+ * breadcrumb lands in the same stream as the other 3WF events
57
+ * (three_way_guard_fired / three_way_floor_used / injection_confirmed /
58
+ * injection_recovered). Both sinks are optional + best-effort; passing neither
59
+ * keeps the pre-3WF-5 behavior.
51
60
  */
52
- export function armThrashGuard(currentTokens, rearmPct, effectiveThreshold, stateDir, logger) {
61
+ export function armThrashGuard(currentTokens, rearmPct, effectiveThreshold, stateDir, logger, emit) {
53
62
  if (!Number.isFinite(currentTokens) || currentTokens <= 0)
54
63
  return;
55
64
  if (!Number.isFinite(rearmPct) || rearmPct <= 0)
@@ -65,11 +74,18 @@ export function armThrashGuard(currentTokens, rearmPct, effectiveThreshold, stat
65
74
  const n = Math.round(rearmPct * effectiveThreshold);
66
75
  setMetaNumber(THRASH_BASELINE_KEY, Math.round(currentTokens), stateDir);
67
76
  setMetaNumber(THRASH_BLOCKED_KEY, Math.round(currentTokens + n), stateDir);
68
- logger?.info("thrasguard_armed", {
77
+ const fields = {
69
78
  baselineTokens: Math.round(currentTokens),
70
79
  blockedUntilTokens: Math.round(currentTokens + n),
71
80
  rearmTokens: n,
72
- });
81
+ };
82
+ logger?.info("thrasguard_armed", fields);
83
+ try {
84
+ emit?.("thrasguard_armed", fields);
85
+ }
86
+ catch {
87
+ /* non-fatal: events.log sink must never break arming */
88
+ }
73
89
  }
74
90
  catch {
75
91
  /* non-fatal: best-effort meta write */
@@ -177,7 +193,13 @@ export function evaluatePendingReduction(runtime, currentTokens, config) {
177
193
  // Remember which event armed us so the consult later in THIS SAME event
178
194
  // does not swallow it (see armedOnEvent).
179
195
  armedOnEvent.set(runtime, currentTokens);
180
- armThrashGuard(currentTokens, config.thrashRearmPct, runtime.effectiveThreshold, runtime.currentStateDir, runtime.logger);
196
+ // 3WF-5: also emit the breadcrumb on the always-on events.log sink so
197
+ // the dashboard Events tab sees it (runtime.logger is debug-gated).
198
+ // Optional-chained: a thin runtime stub without appendEvent stays valid.
199
+ const emit = typeof runtime.appendEvent === "function"
200
+ ? runtime.appendEvent.bind(runtime)
201
+ : undefined;
202
+ armThrashGuard(currentTokens, config.thrashRearmPct, runtime.effectiveThreshold, runtime.currentStateDir, runtime.logger, emit);
181
203
  }
182
204
  }
183
205
  catch {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * context-handler/threeWayTelemetry.fixture.ts — shared fixtures for the 3WF-5
3
+ * telemetry verification tests.
4
+ *
5
+ * Split out so the test file stays under the soft cap, mirroring
6
+ * src/recall/recall3wf.fixture.ts (3WF-3) and injectionConfirm.fixture.ts (3WF-4).
7
+ *
8
+ * These are REAL fixtures, not mocks/stubs. The critical difference from the
9
+ * 3WF-1..4 fixtures: those record `appendEvent` calls into an in-memory array,
10
+ * which proves the CALL happened but says nothing about the wire format or the
11
+ * file the dashboard actually tails. Here `appendEvent` is wired to the REAL
12
+ * `appendEventImpl` (extensions/mega-runtime/append-event.ts) against a temp
13
+ * stateDir, so each breadcrumb is serialized to a real events.log exactly as it
14
+ * is in production. The tests then read that file back and parse the JSON — the
15
+ * `ts` + `event` shape is observed, never simulated.
16
+ */
17
+ import { mkdtempSync, readFileSync, existsSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { VectorStore } from "../../../src/vectorStore.js";
21
+ import { compactSession } from "../../../src/engine.js";
22
+ import { appendEventImpl } from "../../mega-runtime/append-event.js";
23
+ /** Real EngineMessage fixture. */
24
+ export function msg(role, text) {
25
+ return { role, text };
26
+ }
27
+ /** A user-role AgentMessage carrying `text` (the tail-block shape). */
28
+ export function userMsg(text) {
29
+ return { role: "user", content: text, timestamp: 1 };
30
+ }
31
+ /** Fresh isolated state dir per VectorStore. */
32
+ export function freshStore() {
33
+ const dir = mkdtempSync(join(tmpdir(), "mc-3wf5-"));
34
+ return { store: new VectorStore({ dedupSim: 0.9, stateDir: dir }), dir };
35
+ }
36
+ /** Persist N distinct checkpoints with ascending timestamps. */
37
+ export function seed(store, topics, sid) {
38
+ topics.forEach((t, i) => {
39
+ compactSession({
40
+ sessionId: sid,
41
+ messages: [msg("user", t), msg("assistant", "ok")],
42
+ keepFrom: 2,
43
+ timestamp: i + 1,
44
+ }, store);
45
+ });
46
+ }
47
+ /**
48
+ * Read + JSON-parse every line of the REAL events.log written under `stateDir`.
49
+ * PREVENT-001: parse failures are surfaced as null and filtered, never thrown.
50
+ */
51
+ export function readEventsLog(stateDir) {
52
+ const path = join(stateDir, "events.log");
53
+ if (!existsSync(path))
54
+ return [];
55
+ return readFileSync(path, "utf-8")
56
+ .split("\n")
57
+ .filter((l) => l.trim().length > 0)
58
+ .map((line) => {
59
+ try {
60
+ const parsed = JSON.parse(line);
61
+ if (parsed == null || typeof parsed !== "object")
62
+ return null;
63
+ return parsed;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ })
69
+ .filter((e) => e != null);
70
+ }
71
+ /** Silent logger (the debug-gated sink is not under test here). */
72
+ export const noopLogger = {
73
+ info: () => { },
74
+ warn: () => { },
75
+ error: () => { },
76
+ };
77
+ /**
78
+ * MegaRuntime whose `appendEvent` is the REAL events.log writer bound to
79
+ * `stateDir`. Only the fields the 3WF guards touch are populated.
80
+ */
81
+ export function realEventRuntime(store, stateDir, over = {}) {
82
+ const self = {
83
+ store,
84
+ currentStateDir: stateDir,
85
+ pendingRecallBlock: over.pendingRecallBlock,
86
+ pendingMemoryRecallBlock: over.pendingMemoryRecallBlock,
87
+ effectiveThreshold: over.effectiveThreshold ?? 100_000,
88
+ lastCtxWindow: 0,
89
+ logger: noopLogger,
90
+ perfTurnStart: undefined,
91
+ rt: { recallInjectedThisTurn: false, lastCheckpointId: null },
92
+ appendEvent(event, fields) {
93
+ appendEventImpl({ currentStateDir: stateDir }, event, fields);
94
+ },
95
+ };
96
+ return self;
97
+ }
98
+ /** ctx stub with a configurable session id + latest-user query. */
99
+ export function ctxStub(opts = {}) {
100
+ const sessionId = opts.sessionId ?? "sess_3wf5";
101
+ const query = opts.query ?? "dedupe race in store";
102
+ return {
103
+ cwd: "/tmp",
104
+ sessionManager: {
105
+ getSessionId: () => sessionId,
106
+ getEntries: () => [
107
+ {
108
+ type: "message",
109
+ id: "e1",
110
+ parentId: null,
111
+ timestamp: "1",
112
+ message: { role: "user", content: query },
113
+ },
114
+ ],
115
+ },
116
+ };
117
+ }
118
+ /** Config stub: only the flags the 3WF guards read. */
119
+ export function configStub(over = {}) {
120
+ return {
121
+ threeWayFailback: over.threeWayFailback ?? true,
122
+ recallTailInject: over.recallTailInject ?? true,
123
+ thrashRearmPct: over.thrashRearmPct ?? 0.1,
124
+ autoInlineK: 3,
125
+ tier: "custom",
126
+ tierPct: null,
127
+ };
128
+ }
@@ -2,6 +2,7 @@ import { normalizeSessionId } from "../../../src/store.js";
2
2
  import { recall } from "../../../src/engine.js";
3
3
  import { formatRecallBlock } from "../../../src/recall.js";
4
4
  import { vectorStats, vectorList } from "../../../src/vectorStore.js";
5
+ import { buildFloorBlock as sharedFloorBlock, FLOOR_UNAVAILABLE_TEXT, } from "../../../src/failback/floor.js";
5
6
  import { recentUserQuery } from "../../mega-runtime.js";
6
7
  /** One-shot completion marker per MegaRuntime (dies with the runtime). */
7
8
  const guardDone = new WeakMap();
@@ -61,26 +62,19 @@ export function runTriggerGuard(runtime, config, ctx) {
61
62
  /* never throws; best-effort guard */
62
63
  }
63
64
  }
64
- /** Build the provenance floor string from the session's newest checkpoint. */
65
+ /**
66
+ * Build the provenance floor string from the session's newest checkpoint.
67
+ *
68
+ * 3WF-4: the text construction moved to the SHARED pure builder
69
+ * (src/failback/floor.ts) — this keeps the store read (`vectorList`, unfiltered,
70
+ * exactly as 3WF-1 shipped) and the string return type, so output is
71
+ * byte-identical to the pre-refactor version.
72
+ */
65
73
  function buildFloorBlock(runtime, sid) {
66
74
  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.");
75
+ return sharedFloorBlock(vectorList(runtime.store, sid)).text;
81
76
  }
82
77
  catch {
83
- return ("This session has compacted context but recall could not surface a " +
84
- "checkpoint relevant to the current request.");
78
+ return FLOOR_UNAVAILABLE_TEXT;
85
79
  }
86
80
  }
@@ -2,6 +2,7 @@ import { estimateSessionTokens } from "../../src/tokens.js";
2
2
  import { piCompactWouldNoop } from "../mega-pipeline.js";
3
3
  import { buildTailResult } from "./context-handler/tailResult.js";
4
4
  import { runTriggerGuard } from "./context-handler/triggerGuard.js";
5
+ import { confirmInjection } from "./context-handler/injectionConfirm.js";
5
6
  import { persistEpochAndMaintain } from "./context-handler/afterCompact.js";
6
7
  import { appendMirrorAndLedger } from "./context-handler/dbMirrorAppend.js";
7
8
  import { evaluateGate, thrashGuardBlocks } from "./context-handler/gateCheck.js";
@@ -43,7 +44,22 @@ export function registerContextHandler(pi, runtime, config) {
43
44
  // tail message at any view-return point. Returns undefined when nothing
44
45
  // is staged (or the flag is OFF) so the caller falls through to its
45
46
  // normal return.
46
- const tailResult = buildTailResult(runtime, config, messages);
47
+ const composeTail = buildTailResult(runtime, config, messages);
48
+ // 3WF-4 InjectionConfirm: wrap the tail factory so EVERY return point of
49
+ // this handler (gate / replay / debounce / thrash-guard / pipeline /
50
+ // live-trim) is verified — the staged block's marker must be present in
51
+ // the message list pi will send (tail mode), else we re-compose from the
52
+ // runtime's pending blocks and finally fall back to the shared floor.
53
+ // A composition that yields nothing staged (undefined) is passed through
54
+ // untouched, so flag-OFF and no-recall paths are byte-identical.
55
+ const tailResult = config.threeWayFailback
56
+ ? (msgs) => {
57
+ const view = composeTail(msgs);
58
+ if (!view)
59
+ return view;
60
+ return confirmInjection(runtime, config, view, ctx.sessionManager.getSessionId());
61
+ }
62
+ : composeTail;
47
63
  // Always track context for the dashboard/widget, even when auto is off.
48
64
  // (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
49
65
  // null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
@@ -0,0 +1,35 @@
1
+ /** Floor text when the newest checkpoint summary is available (prefix). */
2
+ const WITH_SUMMARY_PREFIX = "The following compacted context is the most recent checkpoint from " +
3
+ "this session (recall found no query-relevant match):\n\n";
4
+ /** Floor text when checkpoints exist but no usable summary does. */
5
+ const NO_SUMMARY_TEXT = "This session has compacted context but recall could not surface a " +
6
+ "checkpoint relevant to the current request; the most recent checkpoint " +
7
+ "summary is unavailable.";
8
+ /** Floor text when the checkpoint read itself failed (hard last resort). */
9
+ export const FLOOR_UNAVAILABLE_TEXT = "This session has compacted context but recall could not surface a " +
10
+ "checkpoint relevant to the current request.";
11
+ /** The newest checkpoint by timestamp (first element wins ties, as before). */
12
+ export function newestCheckpoint(cps) {
13
+ let newest = cps[0];
14
+ for (const cp of cps) {
15
+ if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0))
16
+ newest = cp;
17
+ }
18
+ return newest;
19
+ }
20
+ /**
21
+ * Build the provenance floor from an already-read checkpoint list. Pure: the
22
+ * caller owns the read (and any dedup-status filtering), so both legacy call
23
+ * sites keep byte-identical output.
24
+ */
25
+ export function buildFloorBlock(cps) {
26
+ const summary = newestCheckpoint(cps)?.summary?.trim();
27
+ if (summary) {
28
+ return { text: WITH_SUMMARY_PREFIX + summary, basis: "lastCheckpoint" };
29
+ }
30
+ return { text: NO_SUMMARY_TEXT, basis: "lastCheckpoint" };
31
+ }
32
+ /** The hard last-resort floor (checkpoint read unavailable or threw). */
33
+ export function unavailableFloorBlock() {
34
+ return { text: FLOOR_UNAVAILABLE_TEXT, basis: "none" };
35
+ }
@@ -26,36 +26,22 @@ import { defaultEmbedder, cosineSimilarity } from "../embedder.js";
26
26
  // [] for live sessions). Mirrors vector-search.ts / tieredRouter.ts.
27
27
  import { listCheckpoints } from "../store/sqlite.js";
28
28
  import { RECALL_MIN_COSINE } from "../config.js";
29
- /** Build the provenance floor block from the session's newest checkpoint. */
29
+ import { buildFloorBlock as sharedFloorBlock, unavailableFloorBlock, } from "../failback/floor.js";
30
+ /**
31
+ * Build the provenance floor block from the session's newest checkpoint.
32
+ *
33
+ * 3WF-4: the text construction moved to the SHARED pure builder
34
+ * (src/failback/floor.ts). This wrapper keeps THIS call site's read semantics —
35
+ * `listCheckpoints` filtered to `dedupStatus !== "removed"` — so the output is
36
+ * byte-identical to the pre-refactor 3WF-3 version.
37
+ */
30
38
  function buildFloorBlock(sessionId, store) {
31
39
  try {
32
40
  const cps = listCheckpoints(sessionId, store.stateDir).filter((c) => c.dedupStatus !== "removed");
33
- let newest = cps[0];
34
- for (const cp of cps) {
35
- if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0))
36
- newest = cp;
37
- }
38
- const summary = newest?.summary?.trim();
39
- if (summary) {
40
- return {
41
- text: "The following compacted context is the most recent checkpoint from " +
42
- "this session (recall found no query-relevant match):\n\n" + summary,
43
- basis: "lastCheckpoint",
44
- };
45
- }
46
- return {
47
- text: "This session has compacted context but recall could not surface a " +
48
- "checkpoint relevant to the current request; the most recent checkpoint " +
49
- "summary is unavailable.",
50
- basis: "lastCheckpoint",
51
- };
41
+ return sharedFloorBlock(cps);
52
42
  }
53
43
  catch {
54
- return {
55
- text: "This session has compacted context but recall could not surface a " +
56
- "checkpoint relevant to the current request.",
57
- basis: "none",
58
- };
44
+ return unavailableFloorBlock();
59
45
  }
60
46
  }
61
47
  /**
@@ -296,6 +296,23 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
296
296
  ),
297
297
  ],
298
298
  },
299
+ {
300
+ name: "Three-Way Failback",
301
+ settings: [
302
+ boolDirect(
303
+ "MEGACOMPACT_THREE_WAY_FAILBACK",
304
+ "Three-Way Failback",
305
+ "Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
306
+ true,
307
+ ),
308
+ boolDirect(
309
+ "MEGACOMPACT_RECALL_TAIL_INJECT",
310
+ "Recall Tail Inject",
311
+ "Compose the staged recall block as a trailing user message on the context event (tail inject) instead of the legacy system-prompt prepend. Tail mode keeps the cache prefix stable and is the mode InjectionConfirm verifies against ContextEvent.messages; OFF falls back to the legacy prepend path (verified by string-contains). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
312
+ true,
313
+ ),
314
+ ],
315
+ },
299
316
  VECTOR_CORTEX_SETTINGS,
300
317
  {
301
318
  name: "Cost API",
@@ -0,0 +1,90 @@
1
+ /**
2
+ * context-handler/injectionConfirm.fixture.ts — shared fixtures for the 3WF-4
3
+ * InjectionConfirm tests.
4
+ *
5
+ * Split out so each test file stays under the extensions/300-soft-cap the way
6
+ * src/recall/recall3wf.fixture.ts does for 3WF-3. These are REAL fixtures, not
7
+ * mocks/stubs: a REAL VectorStore over a temp stateDir with REAL checkpoints
8
+ * persisted via compactSession; the MegaRuntime is a minimal typed stub exposing
9
+ * only the fields confirmInjection touches (store, pendingRecallBlock,
10
+ * pendingMemoryRecallBlock, appendEvent), matching the triggerGuard/thrashGuard
11
+ * test conventions.
12
+ */
13
+ import { mkdtempSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
18
+ import { VectorStore } from "../../../src/vectorStore.js";
19
+ import { compactSession } from "../../../src/engine.js";
20
+ import type { MegaRuntime } from "../../mega-runtime.js";
21
+ import type { MegaConfig } from "../../mega-config.js";
22
+
23
+ /** Real EngineMessage fixture. */
24
+ export function msg(role: "user" | "assistant", text: string): any {
25
+ return { role, text };
26
+ }
27
+
28
+ /** A user-role AgentMessage carrying `text` (the tail-block shape). */
29
+ export function userMsg(text: string): AgentMessage {
30
+ return { role: "user", content: text, timestamp: 1 } as unknown as AgentMessage;
31
+ }
32
+
33
+ /** Recorded appendEvent calls, for telemetry assertions. */
34
+ export interface RecordedEvent {
35
+ name: string;
36
+ payload: Record<string, unknown>;
37
+ }
38
+
39
+ /** Fresh isolated state dir per VectorStore. */
40
+ export function freshStore(): { store: VectorStore; dir: string } {
41
+ const dir = mkdtempSync(join(tmpdir(), "mc-inject-"));
42
+ return { store: new VectorStore({ dedupSim: 0.9, stateDir: dir }), dir };
43
+ }
44
+
45
+ /** Persist N distinct checkpoints with ascending timestamps. */
46
+ export function seed(store: VectorStore, topics: string[], sid = "sess_inject"): void {
47
+ topics.forEach((t, i) => {
48
+ compactSession(
49
+ {
50
+ sessionId: sid,
51
+ messages: [msg("user", t), msg("assistant", "ok")],
52
+ keepFrom: 2,
53
+ timestamp: i + 1,
54
+ },
55
+ store,
56
+ );
57
+ });
58
+ }
59
+
60
+ /** Minimal MegaRuntime stub exposing only the confirmInjection touch-points. */
61
+ export function runtimeStub(
62
+ store: VectorStore,
63
+ over: Partial<{
64
+ pendingRecallBlock: string | undefined;
65
+ pendingMemoryRecallBlock: string | undefined;
66
+ }> = {},
67
+ ): { runtime: MegaRuntime; events: RecordedEvent[] } {
68
+ const events: RecordedEvent[] = [];
69
+ const runtime = {
70
+ store,
71
+ pendingRecallBlock: over.pendingRecallBlock,
72
+ pendingMemoryRecallBlock: over.pendingMemoryRecallBlock,
73
+ perfTurnStart: undefined,
74
+ rt: { recallInjectedThisTurn: false },
75
+ appendEvent: (name: string, payload: Record<string, unknown>) => {
76
+ events.push({ name, payload });
77
+ },
78
+ } as unknown as MegaRuntime;
79
+ return { runtime, events };
80
+ }
81
+
82
+ /** Config stub: only the flags confirmInjection reads. */
83
+ export function configStub(
84
+ over: Partial<{ threeWayFailback: boolean; recallTailInject: boolean }> = {},
85
+ ): MegaConfig {
86
+ return {
87
+ threeWayFailback: over.threeWayFailback ?? true,
88
+ recallTailInject: over.recallTailInject ?? true,
89
+ } as unknown as MegaConfig;
90
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * context-handler/injectionConfirm.ts — 3WF-4 InjectionConfirm.
3
+ *
4
+ * QA amendment A3 (binding): pi exposes NO prompt readback API, so the only
5
+ * verifiable proxy for what the provider will actually receive is the pre-LLM
6
+ * message list. In DEFAULT tail mode (`recallTailInject` ON) the staged recall
7
+ * block rides in as a user-role tail message, so we assert the block's marker
8
+ * text is present in the message list we are about to return. In LEGACY prepend
9
+ * mode (`recallTailInject` OFF) the block never enters the message list at all,
10
+ * so the guard degrades to a string-contains check over our own composed return
11
+ * value and never reports a false miss.
12
+ *
13
+ * Recovery ladder when the marker is absent (tail mode only):
14
+ * 1. recomposed — rebuild the view from the runtime's pending blocks via the
15
+ * SAME `buildTailResult` composition the handler uses (self-repair on this
16
+ * event; the user sees nothing).
17
+ * 2. floor — nothing pending either: append the shared provenance floor text
18
+ * (src/failback/floor.ts) as a user-role tail message so the model is never
19
+ * silently left with no compacted-context provenance at all.
20
+ *
21
+ * Stack position: wraps the `tailResult` closure returned by buildTailResult, so
22
+ * EVERY return point of the context handler (gate / replay / debounce /
23
+ * thrash-guard / pipeline / live-trim) is verified with one wiring point.
24
+ *
25
+ * Non-fatal everywhere: any throw degrades to the unverified view (pre-sprint
26
+ * behavior). Flag OFF => the wrapper is never installed (byte-identical).
27
+ */
28
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
29
+ import type { MegaRuntime } from "../../mega-runtime.js";
30
+ import type { MegaConfig } from "../../mega-config.js";
31
+ import type { InjectionVerdict } from "../../../src/failback/types.js";
32
+ import { vectorList } from "../../../src/vectorStore.js";
33
+ import { normalizeSessionId } from "../../../src/store.js";
34
+ import { buildFloorBlock } from "../../../src/failback/floor.js";
35
+ import { withRecallTail } from "../recall-tail.js";
36
+ import { messageContentText } from "./messageText.js";
37
+
38
+ /** A composed context view (the shape every handler return point produces). */
39
+ export interface TailView {
40
+ messages: AgentMessage[];
41
+ }
42
+
43
+ /** The staged blocks + mode this pass verifies (pure inputs, no pi runtime). */
44
+ export interface ConfirmInput {
45
+ /** The staged block text expected to have landed (null => nothing to verify). */
46
+ staged: string | null;
47
+ /** False in legacy prepend mode: verify the return string, not the list. */
48
+ tailMode: boolean;
49
+ }
50
+
51
+ /**
52
+ * The marker substring used to locate a staged block inside a message. The
53
+ * block's first non-empty line, capped, so prompt reshapes (cache striping /
54
+ * message separation) that regroup messages cannot defeat the match, while a
55
+ * genuinely dropped block still fails it.
56
+ */
57
+ export function blockMarker(block: string): string {
58
+ const line = block
59
+ .split("\n")
60
+ .map((l) => l.trim())
61
+ .find((l) => l.length > 0);
62
+ return (line ?? "").slice(0, 80);
63
+ }
64
+
65
+ /**
66
+ * PURE decision function: did the staged block land in this view, and if not,
67
+ * which rung should repair it? Takes the already-extracted message texts so it
68
+ * stays free of pi types and is directly unit-testable.
69
+ */
70
+ export function decideInjection(
71
+ input: ConfirmInput,
72
+ messageTexts: readonly string[],
73
+ hasPendingBlocks: boolean,
74
+ ): InjectionVerdict {
75
+ const marker = input.staged ? blockMarker(input.staged) : "";
76
+ // Nothing staged => nothing to assert; NOT a miss. Injecting a floor here
77
+ // would push provenance text into sessions that never had recall to lose.
78
+ if (!marker) return { landed: true, recovered: "none" };
79
+ const landed = messageTexts.some((t) => t.includes(marker));
80
+ if (landed) return { landed: true, recovered: "none" };
81
+ // Absent: recompose when the runtime still holds pending blocks, else floor.
82
+ return { landed: false, recovered: hasPendingBlocks ? "recomposed" : "floor" };
83
+ }
84
+
85
+ /** Append `text` as a user-role tail message (same shape as recall-tail.ts). */
86
+ function withFloorTail(view: TailView, text: string): TailView {
87
+ const tailMsg = {
88
+ role: "user" as const,
89
+ content: text,
90
+ timestamp: Date.now(),
91
+ } as unknown as AgentMessage;
92
+ return { messages: [...view.messages, tailMsg] };
93
+ }
94
+
95
+ /**
96
+ * Thin caller: verify (and if needed repair) one composed view. Returns the view
97
+ * to actually return from the handler. `sessionId` sources the floor checkpoints.
98
+ *
99
+ * The recompose rung deliberately re-appends via `withRecallTail` onto the
100
+ * ALREADY-COMPOSED view rather than re-running `buildTailResult`: the reshape
101
+ * stages (cache striping / message separation) are the realistic way a tail
102
+ * message gets regrouped away, and re-running the same composition would
103
+ * reproduce the same loss. Appending after the reshape is the actual repair, and
104
+ * it keeps the PREVENT-PI-001/002 tail-append invariant (a single user-role
105
+ * message after a complete prefix can never split a toolCall/toolResult pair).
106
+ */
107
+ export function confirmInjection(
108
+ runtime: MegaRuntime,
109
+ config: MegaConfig,
110
+ view: TailView,
111
+ sessionId: string,
112
+ ): TailView {
113
+ try {
114
+ if (!config.threeWayFailback) return view;
115
+ // What the tail composition was supposed to inject. BOTH staged blocks
116
+ // count: withRecallTail joins recall + memory blocks into one tail
117
+ // message, so either one going missing is a real injection failure.
118
+ const staged =
119
+ runtime.pendingRecallBlock ?? runtime.pendingMemoryRecallBlock ?? null;
120
+ // Can the recompose rung actually re-append? Only when a block is still
121
+ // staged on the runtime. When it is not (blocks consumed between
122
+ // composition and this check), or when withRecallTail declines to append,
123
+ // the ladder falls through to the floor rung below.
124
+ const hasPending =
125
+ runtime.pendingRecallBlock != null ||
126
+ runtime.pendingMemoryRecallBlock != null;
127
+ // Legacy prepend mode: the block is not expected in the message list —
128
+ // verify our composed return value contains it instead (A3 degrade path).
129
+ if (!config.recallTailInject) {
130
+ const composed = view.messages.map(messageContentText).join("\n");
131
+ const marker = staged ? blockMarker(staged) : "";
132
+ runtime.appendEvent("injection_confirmed", {
133
+ mode: "prepend",
134
+ landed: marker ? composed.includes(marker) : true,
135
+ });
136
+ return view;
137
+ }
138
+ const verdict = decideInjection(
139
+ { staged, tailMode: true },
140
+ view.messages.map(messageContentText),
141
+ hasPending,
142
+ );
143
+ if (verdict.landed) {
144
+ runtime.appendEvent("injection_confirmed", { mode: "tail", landed: true });
145
+ return view;
146
+ }
147
+ if (verdict.recovered === "recomposed") {
148
+ const rebuilt = withRecallTail(view.messages, runtime, config);
149
+ // withRecallTail returns the input array unchanged on failure; only
150
+ // treat a genuine append as a recovery.
151
+ if (rebuilt.length > view.messages.length) {
152
+ runtime.appendEvent("injection_recovered", { via: "recomposed" });
153
+ return { messages: rebuilt };
154
+ }
155
+ }
156
+ const floor = buildFloorBlock(
157
+ vectorList(runtime.store, normalizeSessionId(sessionId)),
158
+ );
159
+ runtime.appendEvent("injection_recovered", {
160
+ via: "floor",
161
+ basis: floor.basis,
162
+ });
163
+ return withFloorTail(view, floor.text);
164
+ } catch {
165
+ // Non-fatal: return the unverified view (pre-sprint behavior).
166
+ return view;
167
+ }
168
+ }
@@ -75,6 +75,15 @@ export const ReductionValidator = {
75
75
  * Infinity/NaN into meta (getMetaNumber would read it back as 0). Skip arming
76
76
  * + log instead; the next over-threshold event simply re-fires (pre-sprint
77
77
  * behavior) rather than corrupting the guard.
78
+ *
79
+ * 3WF-5 telemetry: `logger` is the debug-gated runtime logger (mega-compact.log,
80
+ * silent unless config.debug) — which means the breadcrumb was invisible to the
81
+ * dashboard Events tab, whose SSE tail reads the repo's events.log. `emit` is
82
+ * the always-on events.log sink (MegaRuntime.appendEvent), so the armed
83
+ * breadcrumb lands in the same stream as the other 3WF events
84
+ * (three_way_guard_fired / three_way_floor_used / injection_confirmed /
85
+ * injection_recovered). Both sinks are optional + best-effort; passing neither
86
+ * keeps the pre-3WF-5 behavior.
78
87
  */
79
88
  export function armThrashGuard(
80
89
  currentTokens: number,
@@ -82,6 +91,7 @@ export function armThrashGuard(
82
91
  effectiveThreshold: number,
83
92
  stateDir: string,
84
93
  logger?: { info(event: string, fields?: Record<string, unknown>): void },
94
+ emit?: (event: string, fields: Record<string, unknown>) => void,
85
95
  ): void {
86
96
  if (!Number.isFinite(currentTokens) || currentTokens <= 0) return;
87
97
  if (!Number.isFinite(rearmPct) || rearmPct <= 0) return;
@@ -96,11 +106,17 @@ export function armThrashGuard(
96
106
  const n = Math.round(rearmPct * effectiveThreshold);
97
107
  setMetaNumber(THRASH_BASELINE_KEY, Math.round(currentTokens), stateDir);
98
108
  setMetaNumber(THRASH_BLOCKED_KEY, Math.round(currentTokens + n), stateDir);
99
- logger?.info("thrasguard_armed", {
109
+ const fields = {
100
110
  baselineTokens: Math.round(currentTokens),
101
111
  blockedUntilTokens: Math.round(currentTokens + n),
102
112
  rearmTokens: n,
103
- });
113
+ };
114
+ logger?.info("thrasguard_armed", fields);
115
+ try {
116
+ emit?.("thrasguard_armed", fields);
117
+ } catch {
118
+ /* non-fatal: events.log sink must never break arming */
119
+ }
104
120
  } catch {
105
121
  /* non-fatal: best-effort meta write */
106
122
  }
@@ -214,12 +230,20 @@ export function evaluatePendingReduction(
214
230
  // Remember which event armed us so the consult later in THIS SAME event
215
231
  // does not swallow it (see armedOnEvent).
216
232
  armedOnEvent.set(runtime, currentTokens);
233
+ // 3WF-5: also emit the breadcrumb on the always-on events.log sink so
234
+ // the dashboard Events tab sees it (runtime.logger is debug-gated).
235
+ // Optional-chained: a thin runtime stub without appendEvent stays valid.
236
+ const emit =
237
+ typeof runtime.appendEvent === "function"
238
+ ? runtime.appendEvent.bind(runtime)
239
+ : undefined;
217
240
  armThrashGuard(
218
241
  currentTokens,
219
242
  config.thrashRearmPct,
220
243
  runtime.effectiveThreshold,
221
244
  runtime.currentStateDir,
222
245
  runtime.logger,
246
+ emit,
223
247
  );
224
248
  }
225
249
  } catch {
@@ -0,0 +1,164 @@
1
+ /**
2
+ * context-handler/threeWayTelemetry.fixture.ts — shared fixtures for the 3WF-5
3
+ * telemetry verification tests.
4
+ *
5
+ * Split out so the test file stays under the soft cap, mirroring
6
+ * src/recall/recall3wf.fixture.ts (3WF-3) and injectionConfirm.fixture.ts (3WF-4).
7
+ *
8
+ * These are REAL fixtures, not mocks/stubs. The critical difference from the
9
+ * 3WF-1..4 fixtures: those record `appendEvent` calls into an in-memory array,
10
+ * which proves the CALL happened but says nothing about the wire format or the
11
+ * file the dashboard actually tails. Here `appendEvent` is wired to the REAL
12
+ * `appendEventImpl` (extensions/mega-runtime/append-event.ts) against a temp
13
+ * stateDir, so each breadcrumb is serialized to a real events.log exactly as it
14
+ * is in production. The tests then read that file back and parse the JSON — the
15
+ * `ts` + `event` shape is observed, never simulated.
16
+ */
17
+ import { mkdtempSync, readFileSync, existsSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+
21
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
22
+ import { VectorStore } from "../../../src/vectorStore.js";
23
+ import { compactSession } from "../../../src/engine.js";
24
+ import { appendEventImpl } from "../../mega-runtime/append-event.js";
25
+ import type { MegaRuntime } from "../../mega-runtime.js";
26
+ import type { MegaConfig } from "../../mega-config.js";
27
+ import type { Logger } from "../../../src/log.js";
28
+
29
+ /** Real EngineMessage fixture. */
30
+ export function msg(role: "user" | "assistant", text: string): any {
31
+ return { role, text };
32
+ }
33
+
34
+ /** A user-role AgentMessage carrying `text` (the tail-block shape). */
35
+ export function userMsg(text: string): AgentMessage {
36
+ return { role: "user", content: text, timestamp: 1 } as unknown as AgentMessage;
37
+ }
38
+
39
+ /** One parsed events.log line. `ts` + `event` are the contract the dashboard
40
+ * Events tab (server.ts SSE tail of stateDir/events.log) consumes. */
41
+ export interface LoggedEvent {
42
+ ts: unknown;
43
+ event: unknown;
44
+ [k: string]: unknown;
45
+ }
46
+
47
+ /** Fresh isolated state dir per VectorStore. */
48
+ export function freshStore(): { store: VectorStore; dir: string } {
49
+ const dir = mkdtempSync(join(tmpdir(), "mc-3wf5-"));
50
+ return { store: new VectorStore({ dedupSim: 0.9, stateDir: dir }), dir };
51
+ }
52
+
53
+ /** Persist N distinct checkpoints with ascending timestamps. */
54
+ export function seed(store: VectorStore, topics: string[], sid: string): void {
55
+ topics.forEach((t, i) => {
56
+ compactSession(
57
+ {
58
+ sessionId: sid,
59
+ messages: [msg("user", t), msg("assistant", "ok")],
60
+ keepFrom: 2,
61
+ timestamp: i + 1,
62
+ },
63
+ store,
64
+ );
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Read + JSON-parse every line of the REAL events.log written under `stateDir`.
70
+ * PREVENT-001: parse failures are surfaced as null and filtered, never thrown.
71
+ */
72
+ export function readEventsLog(stateDir: string): LoggedEvent[] {
73
+ const path = join(stateDir, "events.log");
74
+ if (!existsSync(path)) return [];
75
+ return readFileSync(path, "utf-8")
76
+ .split("\n")
77
+ .filter((l) => l.trim().length > 0)
78
+ .map((line): LoggedEvent | null => {
79
+ try {
80
+ const parsed: unknown = JSON.parse(line);
81
+ if (parsed == null || typeof parsed !== "object") return null;
82
+ return parsed as LoggedEvent;
83
+ } catch {
84
+ return null;
85
+ }
86
+ })
87
+ .filter((e): e is LoggedEvent => e != null);
88
+ }
89
+
90
+ /** Silent logger (the debug-gated sink is not under test here). */
91
+ export const noopLogger: Logger = {
92
+ info: () => {},
93
+ warn: () => {},
94
+ error: () => {},
95
+ } as unknown as Logger;
96
+
97
+ /**
98
+ * MegaRuntime whose `appendEvent` is the REAL events.log writer bound to
99
+ * `stateDir`. Only the fields the 3WF guards touch are populated.
100
+ */
101
+ export function realEventRuntime(
102
+ store: VectorStore,
103
+ stateDir: string,
104
+ over: Partial<{
105
+ pendingRecallBlock: string | undefined;
106
+ pendingMemoryRecallBlock: string | undefined;
107
+ effectiveThreshold: number;
108
+ }> = {},
109
+ ): MegaRuntime {
110
+ const self = {
111
+ store,
112
+ currentStateDir: stateDir,
113
+ pendingRecallBlock: over.pendingRecallBlock,
114
+ pendingMemoryRecallBlock: over.pendingMemoryRecallBlock,
115
+ effectiveThreshold: over.effectiveThreshold ?? 100_000,
116
+ lastCtxWindow: 0,
117
+ logger: noopLogger,
118
+ perfTurnStart: undefined,
119
+ rt: { recallInjectedThisTurn: false, lastCheckpointId: null as string | null },
120
+ appendEvent(event: string, fields: Record<string, unknown>): void {
121
+ appendEventImpl({ currentStateDir: stateDir }, event, fields);
122
+ },
123
+ };
124
+ return self as unknown as MegaRuntime;
125
+ }
126
+
127
+ /** ctx stub with a configurable session id + latest-user query. */
128
+ export function ctxStub(opts: { sessionId?: string; query?: string } = {}): any {
129
+ const sessionId = opts.sessionId ?? "sess_3wf5";
130
+ const query = opts.query ?? "dedupe race in store";
131
+ return {
132
+ cwd: "/tmp",
133
+ sessionManager: {
134
+ getSessionId: () => sessionId,
135
+ getEntries: () => [
136
+ {
137
+ type: "message",
138
+ id: "e1",
139
+ parentId: null,
140
+ timestamp: "1",
141
+ message: { role: "user", content: query },
142
+ },
143
+ ],
144
+ },
145
+ };
146
+ }
147
+
148
+ /** Config stub: only the flags the 3WF guards read. */
149
+ export function configStub(
150
+ over: Partial<{
151
+ threeWayFailback: boolean;
152
+ recallTailInject: boolean;
153
+ thrashRearmPct: number;
154
+ }> = {},
155
+ ): MegaConfig {
156
+ return {
157
+ threeWayFailback: over.threeWayFailback ?? true,
158
+ recallTailInject: over.recallTailInject ?? true,
159
+ thrashRearmPct: over.thrashRearmPct ?? 0.1,
160
+ autoInlineK: 3,
161
+ tier: "custom",
162
+ tierPct: null,
163
+ } as unknown as MegaConfig;
164
+ }
@@ -30,6 +30,10 @@ import { normalizeSessionId } from "../../../src/store.js";
30
30
  import { recall } from "../../../src/engine.js";
31
31
  import { formatRecallBlock } from "../../../src/recall.js";
32
32
  import { vectorStats, vectorList } from "../../../src/vectorStore.js";
33
+ import {
34
+ buildFloorBlock as sharedFloorBlock,
35
+ FLOOR_UNAVAILABLE_TEXT,
36
+ } from "../../../src/failback/floor.js";
33
37
  import { recentUserQuery } from "../../mega-runtime.js";
34
38
 
35
39
  /** One-shot completion marker per MegaRuntime (dies with the runtime). */
@@ -108,30 +112,18 @@ export function runTriggerGuard(
108
112
  }
109
113
  }
110
114
 
111
- /** Build the provenance floor string from the session's newest checkpoint. */
115
+ /**
116
+ * Build the provenance floor string from the session's newest checkpoint.
117
+ *
118
+ * 3WF-4: the text construction moved to the SHARED pure builder
119
+ * (src/failback/floor.ts) — this keeps the store read (`vectorList`, unfiltered,
120
+ * exactly as 3WF-1 shipped) and the string return type, so output is
121
+ * byte-identical to the pre-refactor version.
122
+ */
112
123
  function buildFloorBlock(runtime: MegaRuntime, sid: string): string {
113
124
  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
- );
125
+ return sharedFloorBlock(vectorList(runtime.store, sid)).text;
131
126
  } catch {
132
- return (
133
- "This session has compacted context but recall could not surface a " +
134
- "checkpoint relevant to the current request."
135
- );
127
+ return FLOOR_UNAVAILABLE_TEXT;
136
128
  }
137
129
  }
@@ -24,6 +24,7 @@ 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
26
  import { runTriggerGuard } from "./context-handler/triggerGuard.js";
27
+ import { confirmInjection } from "./context-handler/injectionConfirm.js";
27
28
  import { persistEpochAndMaintain } from "./context-handler/afterCompact.js";
28
29
  import { appendMirrorAndLedger } from "./context-handler/dbMirrorAppend.js";
29
30
  import { evaluateGate, thrashGuardBlocks } from "./context-handler/gateCheck.js";
@@ -72,7 +73,21 @@ export function registerContextHandler(
72
73
  // tail message at any view-return point. Returns undefined when nothing
73
74
  // is staged (or the flag is OFF) so the caller falls through to its
74
75
  // normal return.
75
- const tailResult = buildTailResult(runtime, config, messages);
76
+ const composeTail = buildTailResult(runtime, config, messages);
77
+ // 3WF-4 InjectionConfirm: wrap the tail factory so EVERY return point of
78
+ // this handler (gate / replay / debounce / thrash-guard / pipeline /
79
+ // live-trim) is verified — the staged block's marker must be present in
80
+ // the message list pi will send (tail mode), else we re-compose from the
81
+ // runtime's pending blocks and finally fall back to the shared floor.
82
+ // A composition that yields nothing staged (undefined) is passed through
83
+ // untouched, so flag-OFF and no-recall paths are byte-identical.
84
+ const tailResult: typeof composeTail = config.threeWayFailback
85
+ ? (msgs) => {
86
+ const view = composeTail(msgs);
87
+ if (!view) return view;
88
+ return confirmInjection(runtime, config, view, ctx.sessionManager.getSessionId());
89
+ }
90
+ : composeTail;
76
91
  // Always track context for the dashboard/widget, even when auto is off.
77
92
  // (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
78
93
  // null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.87",
3
+ "version": "0.21.0",
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,71 @@
1
+ /**
2
+ * src/failback/floor.ts — the SHARED pure provenance-floor builder (3WF-4).
3
+ *
4
+ * Consolidation refactor (ZERO behavior change). Before this module the same
5
+ * floor text was built twice:
6
+ * (a) `extensions/mega-events/context-handler/triggerGuard.ts` (3WF-1) —
7
+ * read checkpoints via `vectorList` (unfiltered), returned a bare string;
8
+ * (b) `src/recall/validator.ts` (3WF-3) — read checkpoints via
9
+ * `listCheckpoints` filtered to `dedupStatus !== "removed"`, returned a
10
+ * `FloorBlock`.
11
+ * The three text variants were byte-identical between the two; only the
12
+ * checkpoint READ differed. So this module takes the already-read checkpoint
13
+ * list from the caller (staying pure — no store, no pi types, no I/O) and each
14
+ * call site keeps its own read semantics. Output is byte-identical to both.
15
+ *
16
+ * 3WF-4's InjectionConfirm is the third consumer: when neither the message list
17
+ * nor the runtime's pending blocks yield a block, it needs the SAME last-resort
18
+ * floor text rather than a fourth copy.
19
+ *
20
+ * Non-fatal by construction: every branch returns a `FloorBlock`; the `none`
21
+ * basis carries the shortest text (used when the checkpoint read itself threw).
22
+ */
23
+ import type { StoredCheckpoint } from "../store.js";
24
+ import type { FloorBlock } from "./types.js";
25
+
26
+ /** Floor text when the newest checkpoint summary is available (prefix). */
27
+ const WITH_SUMMARY_PREFIX =
28
+ "The following compacted context is the most recent checkpoint from " +
29
+ "this session (recall found no query-relevant match):\n\n";
30
+
31
+ /** Floor text when checkpoints exist but no usable summary does. */
32
+ const NO_SUMMARY_TEXT =
33
+ "This session has compacted context but recall could not surface a " +
34
+ "checkpoint relevant to the current request; the most recent checkpoint " +
35
+ "summary is unavailable.";
36
+
37
+ /** Floor text when the checkpoint read itself failed (hard last resort). */
38
+ export const FLOOR_UNAVAILABLE_TEXT =
39
+ "This session has compacted context but recall could not surface a " +
40
+ "checkpoint relevant to the current request.";
41
+
42
+ /** The newest checkpoint by timestamp (first element wins ties, as before). */
43
+ export function newestCheckpoint(
44
+ cps: readonly StoredCheckpoint[],
45
+ ): StoredCheckpoint | undefined {
46
+ let newest = cps[0];
47
+ for (const cp of cps) {
48
+ if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0)) newest = cp;
49
+ }
50
+ return newest;
51
+ }
52
+
53
+ /**
54
+ * Build the provenance floor from an already-read checkpoint list. Pure: the
55
+ * caller owns the read (and any dedup-status filtering), so both legacy call
56
+ * sites keep byte-identical output.
57
+ */
58
+ export function buildFloorBlock(
59
+ cps: readonly StoredCheckpoint[],
60
+ ): FloorBlock {
61
+ const summary = newestCheckpoint(cps)?.summary?.trim();
62
+ if (summary) {
63
+ return { text: WITH_SUMMARY_PREFIX + summary, basis: "lastCheckpoint" };
64
+ }
65
+ return { text: NO_SUMMARY_TEXT, basis: "lastCheckpoint" };
66
+ }
67
+
68
+ /** The hard last-resort floor (checkpoint read unavailable or threw). */
69
+ export function unavailableFloorBlock(): FloorBlock {
70
+ return { text: FLOOR_UNAVAILABLE_TEXT, basis: "none" };
71
+ }
@@ -103,6 +103,22 @@ export interface RecallCandidate {
103
103
  source: "vector" | "fts5" | "recency";
104
104
  }
105
105
 
106
+ /**
107
+ * Verdict of an InjectionConfirm pass (3WF-4). There is NO prompt readback API
108
+ * in pi, so the only verifiable proxy for what the provider will receive is the
109
+ * pre-LLM message list (`ContextEvent.messages`, transformed). `landed` is true
110
+ * when the staged block's text was found there (tail mode) or in the composed
111
+ * return string (legacy prepend mode). `recovered` records which repair rung
112
+ * this event used: none (already landed), recomposed (rebuilt from the runtime's
113
+ * pending blocks), or floor (last-resort provenance text).
114
+ */
115
+ export interface InjectionVerdict {
116
+ /** True when the staged block text is present in the verified view. */
117
+ landed: boolean;
118
+ /** Which repair rung ran to make the block present. */
119
+ recovered: "none" | "recomposed" | "floor";
120
+ }
121
+
106
122
  /**
107
123
  * Outcome of the three-source recall vote. `winners` are the agreed candidates
108
124
  * (ranked), `votes` counts how many distinct sources named each checkpointId,
@@ -28,6 +28,10 @@ import { listCheckpoints } from "../store/sqlite.js";
28
28
  import { RECALL_MIN_COSINE } from "../config.js";
29
29
  import type { VectorStore } from "../vectorStore.js";
30
30
  import type { RecallCandidate, FloorBlock } from "../failback/types.js";
31
+ import {
32
+ buildFloorBlock as sharedFloorBlock,
33
+ unavailableFloorBlock,
34
+ } from "../failback/floor.js";
31
35
 
32
36
  /** Options for the recall validator. */
33
37
  export interface ValidateOptions {
@@ -54,39 +58,22 @@ export type ValidationOutcome =
54
58
  | { kind: "candidate"; candidate: RecallCandidate }
55
59
  | { kind: "floor"; floor: FloorBlock };
56
60
 
57
- /** Build the provenance floor block from the session's newest checkpoint. */
61
+ /**
62
+ * Build the provenance floor block from the session's newest checkpoint.
63
+ *
64
+ * 3WF-4: the text construction moved to the SHARED pure builder
65
+ * (src/failback/floor.ts). This wrapper keeps THIS call site's read semantics —
66
+ * `listCheckpoints` filtered to `dedupStatus !== "removed"` — so the output is
67
+ * byte-identical to the pre-refactor 3WF-3 version.
68
+ */
58
69
  function buildFloorBlock(sessionId: string, store: VectorStore): FloorBlock {
59
70
  try {
60
71
  const cps = listCheckpoints(sessionId, store.stateDir).filter(
61
72
  (c) => c.dedupStatus !== "removed",
62
73
  );
63
- let newest = cps[0];
64
- for (const cp of cps) {
65
- if (!newest || (cp.timestamp ?? 0) > (newest.timestamp ?? 0)) newest = cp;
66
- }
67
- const summary = newest?.summary?.trim();
68
- if (summary) {
69
- return {
70
- text:
71
- "The following compacted context is the most recent checkpoint from " +
72
- "this session (recall found no query-relevant match):\n\n" + summary,
73
- basis: "lastCheckpoint",
74
- };
75
- }
76
- return {
77
- text:
78
- "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
- basis: "lastCheckpoint",
82
- };
74
+ return sharedFloorBlock(cps);
83
75
  } catch {
84
- return {
85
- text:
86
- "This session has compacted context but recall could not surface a " +
87
- "checkpoint relevant to the current request.",
88
- basis: "none",
89
- };
76
+ return unavailableFloorBlock();
90
77
  }
91
78
  }
92
79