pi-mega-compact 0.7.7 → 0.7.9

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.
Files changed (114) hide show
  1. package/README.md +11 -12
  2. package/dist/extensions/dashboard-server/helpers.js +37 -0
  3. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  4. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  5. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  6. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  7. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  8. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  9. package/dist/extensions/dashboard-server/html/script.js +259 -0
  10. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  11. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  12. package/dist/extensions/dashboard-server/html-template.js +41 -0
  13. package/dist/extensions/dashboard-server/html.js +756 -0
  14. package/dist/extensions/dashboard-server/index-reader.js +133 -0
  15. package/dist/extensions/dashboard-server/server.js +370 -0
  16. package/dist/extensions/dashboard-server/snapshot.js +43 -0
  17. package/dist/extensions/dashboard-server/state.js +30 -0
  18. package/dist/extensions/dashboard-server/types.js +5 -0
  19. package/dist/extensions/dashboard-server.js +7 -1315
  20. package/dist/extensions/mega-commands.js +162 -134
  21. package/dist/extensions/mega-compact.test.js +292 -24
  22. package/dist/extensions/mega-config.js +10 -0
  23. package/dist/extensions/mega-conflict-cmds.js +5 -1
  24. package/dist/extensions/mega-dashboard-cmds.js +29 -22
  25. package/dist/extensions/mega-db-cmds.js +11 -2
  26. package/dist/extensions/mega-events/agent-handlers.js +173 -0
  27. package/dist/extensions/mega-events/compact-handlers.js +133 -0
  28. package/dist/extensions/mega-events/context-handler.js +249 -0
  29. package/dist/extensions/mega-events/register.js +21 -0
  30. package/dist/extensions/mega-events/session-handlers.js +142 -0
  31. package/dist/extensions/mega-events.js +15 -652
  32. package/dist/extensions/mega-pipeline/compact.js +324 -0
  33. package/dist/extensions/mega-pipeline/memory-review.js +38 -0
  34. package/dist/extensions/mega-pipeline/recall.js +147 -0
  35. package/dist/extensions/mega-pipeline.js +9 -480
  36. package/dist/extensions/mega-runtime/helpers.js +40 -0
  37. package/dist/extensions/mega-runtime/query.js +29 -0
  38. package/dist/extensions/mega-runtime/state.js +711 -0
  39. package/dist/extensions/mega-runtime/widget.js +197 -0
  40. package/dist/extensions/mega-runtime.js +15 -932
  41. package/dist/src/store/sqlite/checkpoints.js +145 -0
  42. package/dist/src/store/sqlite/connection.js +35 -0
  43. package/dist/src/store/sqlite/dedup-mirror.js +64 -0
  44. package/dist/src/store/sqlite/foundation.js +38 -0
  45. package/dist/src/store/sqlite/global-index.js +224 -0
  46. package/dist/src/store/sqlite/index-store.js +167 -0
  47. package/dist/src/store/sqlite/maintenance.js +235 -0
  48. package/dist/src/store/sqlite/memories.js +164 -0
  49. package/dist/src/store/sqlite/memory.js +54 -0
  50. package/dist/src/store/sqlite/meta.js +82 -0
  51. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  52. package/dist/src/store/sqlite/model-snapshots.js +47 -0
  53. package/dist/src/store/sqlite/raptor.js +57 -0
  54. package/dist/src/store/sqlite/raw-transcript.js +134 -0
  55. package/dist/src/store/sqlite/schema.js +250 -0
  56. package/dist/src/store/sqlite/session-state.js +28 -0
  57. package/dist/src/store/sqlite/sessions.js +39 -0
  58. package/dist/src/store/sqlite/stats.js +66 -0
  59. package/dist/src/store/sqlite/transaction.js +19 -0
  60. package/dist/src/store/sqlite/utils.js +120 -0
  61. package/dist/src/store/sqlite.js +20 -1607
  62. package/dist/src/vectorStore/add.js +260 -0
  63. package/dist/src/vectorStore/dedup.js +52 -0
  64. package/dist/src/vectorStore/index.js +10 -0
  65. package/dist/src/vectorStore/queries.js +83 -0
  66. package/dist/src/vectorStore/search.js +95 -0
  67. package/dist/src/vectorStore/session.js +19 -0
  68. package/dist/src/vectorStore/store.js +105 -0
  69. package/dist/src/vectorStore/types.js +6 -0
  70. package/dist/src/vectorStore/utils.js +23 -0
  71. package/extensions/dashboard-server/html.ts +758 -0
  72. package/extensions/dashboard-server/index-reader.ts +130 -0
  73. package/extensions/dashboard-server/server.ts +358 -0
  74. package/extensions/dashboard-server/snapshot.ts +44 -0
  75. package/extensions/dashboard-server/state.ts +33 -0
  76. package/extensions/dashboard-server/types.ts +134 -0
  77. package/extensions/dashboard-server.ts +7 -1431
  78. package/extensions/mega-commands.ts +33 -10
  79. package/extensions/mega-compact.test.ts +453 -37
  80. package/extensions/mega-config.ts +22 -0
  81. package/extensions/mega-conflict-cmds.ts +6 -2
  82. package/extensions/mega-dashboard-cmds.ts +30 -23
  83. package/extensions/mega-db-cmds.ts +11 -3
  84. package/extensions/mega-events/agent-handlers.ts +214 -0
  85. package/extensions/mega-events/compact-handlers.ts +164 -0
  86. package/extensions/mega-events/context-handler.ts +290 -0
  87. package/extensions/mega-events/register.ts +37 -0
  88. package/extensions/mega-events/session-handlers.ts +165 -0
  89. package/extensions/mega-events.ts +15 -732
  90. package/extensions/mega-pipeline/compact.ts +366 -0
  91. package/extensions/mega-pipeline/memory-review.ts +46 -0
  92. package/extensions/mega-pipeline/recall.ts +165 -0
  93. package/extensions/mega-pipeline.ts +9 -537
  94. package/extensions/mega-runtime/helpers.ts +68 -0
  95. package/extensions/mega-runtime/query.ts +29 -0
  96. package/extensions/mega-runtime/state.ts +797 -0
  97. package/extensions/mega-runtime/widget.ts +258 -0
  98. package/extensions/mega-runtime.ts +15 -1076
  99. package/package.json +4 -3
  100. package/src/store/sqlite/checkpoints.ts +204 -0
  101. package/src/store/sqlite/dedup-mirror.ts +114 -0
  102. package/src/store/sqlite/foundation.ts +63 -0
  103. package/src/store/sqlite/global-index.ts +305 -0
  104. package/src/store/sqlite/maintenance.ts +294 -0
  105. package/src/store/sqlite/memories.ts +217 -0
  106. package/src/store/sqlite/meta.ts +108 -0
  107. package/src/store/sqlite/model-snapshots.ts +83 -0
  108. package/src/store/sqlite/raptor.ts +107 -0
  109. package/src/store/sqlite/raw-transcript.ts +221 -0
  110. package/src/store/sqlite/schema.ts +258 -0
  111. package/src/store/sqlite/session-state.ts +38 -0
  112. package/src/store/sqlite/stats.ts +127 -0
  113. package/src/store/sqlite/utils.ts +125 -0
  114. package/src/store/sqlite.ts +20 -2204
@@ -0,0 +1,290 @@
1
+ /**
2
+ * mega-events/context-handler.ts — the context event handler (auto-trigger).
3
+ *
4
+ * Handles the live-trim compaction pipeline: DB-mirror append, fast-gate
5
+ * threshold check, pipeline invocation, checkpoint epoch write, dedup, and
6
+ * the live-trim message reconstruction that feeds pi's transformContext.
7
+ */
8
+ import type {
9
+ ExtensionAPI,
10
+ ExtensionContext,
11
+ ContextEvent,
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
14
+ import {
15
+ openStore,
16
+ appendRawTranscript,
17
+ writeCheckpointEpoch,
18
+ type CheckpointEpoch,
19
+ type RawTranscriptRow,
20
+ } from "../../src/store/sqlite.js";
21
+ import { epochIdFor } from "../../src/mirror/epoch.js";
22
+ import { autoCompactCheck } from "../../src/compact.js";
23
+ import { estimateSessionTokens } from "../../src/tokens.js";
24
+ import { type MegaRuntime } from "../mega-runtime.js";
25
+ import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
26
+ import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
27
+ import {
28
+ pressureFromPct,
29
+ pressureRatio,
30
+ type MegaConfig,
31
+ } from "../mega-config.js";
32
+ import { createHash } from "node:crypto";
33
+
34
+ /**
35
+ * Convert a pi AgentMessage to a RawTranscriptRow for the DB mirror.
36
+ * content_bytes is canonical JSON (sorted keys) for deterministic hashing.
37
+ * Returns null if the message has no usable content.
38
+ */
39
+ function toRawTranscriptRow(
40
+ msg: AgentMessage,
41
+ sessionId: string,
42
+ epochId: string,
43
+ ): RawTranscriptRow | null {
44
+ // Narrow to Message union (has content + timestamp).
45
+ const m = msg as { role?: string; content?: unknown; timestamp?: number; toolName?: string };
46
+ const content = m.content;
47
+ if (content == null || content === "") return null;
48
+ // Canonical form: sort object keys for deterministic hashing.
49
+ const contentBytes = typeof content === "string"
50
+ ? content
51
+ : JSON.stringify(content, Object.keys(content as object).sort());
52
+ const contentHash = createHash("sha256").update(contentBytes).digest("hex");
53
+ return {
54
+ contentHash,
55
+ sessionId,
56
+ seq: 0, // assigned by appendRawTranscript (COALESCE(MAX(seq),0)+1)
57
+ role: m.role ?? "unknown",
58
+ contentBytes,
59
+ toolName: m.toolName ?? null,
60
+ messageTimestamp: m.timestamp ?? null,
61
+ checkpointEpoch: epochId,
62
+ };
63
+ }
64
+
65
+ /** Register the context event handler (live-trim auto-trigger). */
66
+ export function registerContextHandler(
67
+ pi: ExtensionAPI,
68
+ runtime: MegaRuntime,
69
+ config: MegaConfig,
70
+ ): void {
71
+ // ---- Auto-trigger: live trim (compact and continue) + native durable ----
72
+ // S16 redesign: we NO LONGER call ctx.compact() from the auto-trigger by
73
+ // default. That mapped to pi's MANUAL compaction path, which abort()s the
74
+ // in-flight turn (agent-session.js:1345) and stops the agent. Instead:
75
+ // - LIVE: return { messages: trimmedView } from the context event. This
76
+ // feeds pi's transformContext (sdk.js:226 → agent-loop.js:180) so the
77
+ // model sees a compacted window EVERY LLM call, with no abort. The turn
78
+ // continues. We persist our recall checkpoint (the durable value) first.
79
+ // - DURABLE: pi's NATIVE auto-compaction fires at agent-end
80
+ // (agent-session.js:1565), continues (return hasQueuedMessages()), and
81
+ // emits session_before_compact — where OUR driveNativeCompaction supplies
82
+ // the summary and pi truncates the transcript on disk. No ctx.compact().
83
+ // Legacy: MEGACOMPACT_LEGACY_DURABLE_TRIM=true restores the v0.4.28 ctx.compact
84
+ // path (kept one release as rollback).
85
+ pi.on("context", async (event: ContextEvent, ctx: ExtensionContext) => {
86
+ if (!config.auto) return;
87
+ const usage = ctx.getContextUsage();
88
+ const pct = usage?.percent;
89
+ // Always track context for the dashboard, even if we return early below.
90
+ runtime.lastCtxTokens = usage?.tokens ?? null;
91
+ runtime.lastCtxPercent = pct ?? null;
92
+ runtime.lastCtxWindow = usage?.contextWindow ?? 0;
93
+ runtime.snapshot(ctx);
94
+
95
+ const messages = event.messages;
96
+ const view = runtime.engineView(messages);
97
+ const currentTokens =
98
+ usage?.tokens ??
99
+ estimateSessionTokens(view) ??
100
+ Math.round(((pct ?? 0) / 100) * (usage?.contextWindow ?? 0));
101
+
102
+ // S27 DB-mirror: append ALL incoming messages to raw_transcript.
103
+ // Runs BEFORE fast-gate so every message is captured, even if we
104
+ // don't compact this turn. Append is idempotent (content_hash PK).
105
+ if (config.dbMirror) {
106
+ try {
107
+ const db = openStore(runtime.currentStateDir);
108
+ const epochId = epochIdFor(runtime.rt.sessionId);
109
+ for (const msg of messages) {
110
+ const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId);
111
+ if (raw) appendRawTranscript(db, raw);
112
+ }
113
+ } catch (e) {
114
+ runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
115
+ }
116
+ }
117
+
118
+ // S29 FAST GATE: drive the auto-trigger off the context % (the number the
119
+ // menu bar shows), NOT the token count — the model under-reports tokens,
120
+ // so a token-only gate misses the overshoot that causes max-output-tokens
121
+ // truncation. The fire point is the tier's percent threshold (tierPct)
122
+ // unless overridden by MEGACOMPACT_AUTO_PCT_TRIGGER. `custom` (absolute
123
+ // MEGACOMPACT_THRESHOLD_TOKENS, tierPct null) is an explicit opt-out of
124
+ // percent scaling — it keeps the token gate. When pct is unavailable
125
+ // (window unknown / a model that doesn't report percent) a tiered config
126
+ // falls back to the token gate (S27 boot-fallback guarantee) instead of
127
+ // skipping compaction — a percent-only gate would regress that.
128
+ let gatePassed = false;
129
+ if (config.tierPct != null && pct != null) {
130
+ const firePct = config.autoPctTrigger ?? config.tierPct;
131
+ gatePassed = pct / 100 >= firePct;
132
+ } else {
133
+ // custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
134
+ if (currentTokens < runtime.effectiveThreshold) {
135
+ runtime.diagCtxFastGate++;
136
+ return;
137
+ }
138
+ const check = autoCompactCheck(currentTokens, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
139
+ if (!check.shouldCompact) {
140
+ runtime.diagCtxNoCompact++;
141
+ return;
142
+ }
143
+ gatePassed = true;
144
+ }
145
+ if (!gatePassed) {
146
+ runtime.diagCtxFastGate++;
147
+ return;
148
+ }
149
+
150
+ // Debounce so we don't fire on every context event past threshold.
151
+ const now = Date.now();
152
+ if (now < runtime.debounceUntil) {
153
+ runtime.diagCtxDebounce++;
154
+ return;
155
+ }
156
+ runtime.debounceUntil = now + 2000;
157
+
158
+ // Adaptive compression (Fix E): scale compression strength + keepFrom depth
159
+ // with how close we are to the model context limit. Null-safe: when the
160
+ // token-fallback path ran (pct unavailable) use the token-basis pressure
161
+ // (the same basis the runtime `pressure` getter uses for custom/no-window).
162
+ const pressure = pct != null ? pressureFromPct(pct) : pressureRatio(currentTokens, runtime.effectiveThreshold);
163
+ const ran = runCompact(pi, runtime, config, ctx, messages, {
164
+ compressionPressure: pressure,
165
+ });
166
+ if (ran.skipped) {
167
+ runtime.diagCtxRunSkipped++;
168
+ return;
169
+ }
170
+
171
+ // S27 DB-mirror: write checkpoint_epoch with deterministic nonce.
172
+ // This makes the cache key stable across identical compactions.
173
+ if (config.dbMirror) {
174
+ try {
175
+ const db = openStore(runtime.currentStateDir);
176
+ const cpId = ran.result.checkpointId ?? `epoch-${Date.now()}`;
177
+ const epoch: CheckpointEpoch = {
178
+ epochId: epochIdFor(cpId),
179
+ sessionId: runtime.rt.sessionId,
180
+ startedSeq: 0,
181
+ committedSeq: ran.result.compactedFrom,
182
+ checkpointId: cpId,
183
+ cutIndex: ran.result.compactedFrom,
184
+ summaryMessageText: ran.result.summary,
185
+ createdAt: Date.now(),
186
+ };
187
+ writeCheckpointEpoch(db, epoch);
188
+ // S27 Task 6: Fire-and-forget dedup pipeline.
189
+ // Deduplicates raw_transcript rows for the compacted range.
190
+ try {
191
+ const { dedupTranscript } = await import("../../src/mirror/dedup.js");
192
+ dedupTranscript(
193
+ db,
194
+ runtime.rt.sessionId,
195
+ 0,
196
+ ran.result.compactedFrom,
197
+ );
198
+ } catch (_dedupErr) {
199
+ // Fire-and-forget: dedup failure is non-fatal
200
+ }
201
+ } catch (e) {
202
+ runtime.logger.warn("db-mirror-epoch-fail", { error: String(e) });
203
+ }
204
+ }
205
+
206
+ // LEGACY path (rollback): v0.4.28 ctx.compact() + the no-op gate. The
207
+ // manual compact path aborts the in-flight turn — only used behind the flag.
208
+ // Read live from env (in addition to the load-time config) so the flag can be
209
+ // toggled per-test without reloading the module; config.legacyDurableTrim is
210
+ // the cached default. (Mirrors how piCompactWouldNoop re-reads its floor.)
211
+ const legacy =
212
+ config.legacyDurableTrim ||
213
+ process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM === "true" ||
214
+ process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM === "1";
215
+ if (legacy) {
216
+ // COMPACT-DEDUP FIX: same race guard as the agent_end path. Skip when a
217
+ // NATIVE compaction just fired (avoids racing pi and surfacing a spurious
218
+ // "Already compacted" / "Nothing to compact" toast). Uses lastNativeCompactAt
219
+ // (NOT lastCompactAt, which runCompact also stamps for our own checkpoint).
220
+ const sinceCompact = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
221
+ if (sinceCompact < 10_000 || piCompactWouldNoop(ctx)) return;
222
+ ctx.compact({ customInstructions: undefined }); // race-guarded by lastNativeCompactAt cooldown (ctx.compact returns void → not catchable; the cooldown prevents the call)
223
+ return;
224
+ }
225
+
226
+ // S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
227
+ // Non-destructive: pi keeps the real transcript; only this LLM call sees the
228
+ // trimmed window. We compute the cut on the engine view (pure, tested) then
229
+ // slice the ORIGINAL pi AgentMessage[] from that index (lossless alignment,
230
+ // mirroring dropCompactedRange) and prepend a user-role summary message.
231
+ // A build failure or unsafe cut returns nothing (no trim this call — the
232
+ // next context event retries). The anchor floor is read live from env (the
233
+ // config value is the cached default) so it can be tuned per-test / per-run
234
+ // without reloading the module.
235
+ try {
236
+ const anchorEnv = process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
237
+ const anchorUserMessages =
238
+ anchorEnv != null &&
239
+ anchorEnv !== "" &&
240
+ Number.isFinite(Number(anchorEnv))
241
+ ? Number(anchorEnv)
242
+ : config.anchorUserMessages;
243
+ const cut = computeLiveTrimCut(view, {
244
+ compactedFrom: ran.result.compactedFrom,
245
+ summary: ran.result.summary,
246
+ anchorUserMessages,
247
+ });
248
+ if (cut === null) {
249
+ runtime.diagCtxCutNull++;
250
+ runtime.logger.info("live-trim-skip", {
251
+ sessionId: runtime.rt.sessionId,
252
+ compactedFrom: ran.result.compactedFrom,
253
+ viewLen: view.length,
254
+ anchorUserMessages,
255
+ });
256
+ return; // unsafe / below anchor floor — no trim this call
257
+ }
258
+ const summaryMsg = liveTrimSummaryMessage({
259
+ compactedFrom: ran.result.compactedFrom,
260
+ summary: ran.result.summary,
261
+ anchorUserMessages: config.anchorUserMessages,
262
+ });
263
+ // Synthesize a user-role AgentMessage carrying the compacted summary.
264
+ const summaryAgentMsg = {
265
+ role: "user" as const,
266
+ content: summaryMsg.text,
267
+ timestamp: Date.now(),
268
+ } as unknown as AgentMessage;
269
+ const recent = messages.slice(cut); // guardrails-allow PREVENT-PI-002: `cut` is the pre-sanitized `compactedFrom` produced by src/boundary.ts computeDropRange, so the preserved run begins on a toolPair-safe index.
270
+ runtime.snapshot(ctx);
271
+ // DIAG (team-run relief): confirm the live trim actually fires + how big
272
+ // the window still is. The return is non-durable (per-LLM-call only), so
273
+ // this is the signal that the model is being fed a compacted view while
274
+ // the on-disk transcript + context meter keep growing.
275
+ runtime.diagLiveTrimFires++;
276
+ runtime.logger.info("live-trim", {
277
+ sessionId: runtime.rt.sessionId,
278
+ inputMsgs: messages.length,
279
+ outputMsgs: recent.length + 1,
280
+ compactedFrom: cut,
281
+ ctxPct: pct,
282
+ ctxTokens: usage?.tokens ?? null,
283
+ });
284
+ return { messages: [summaryAgentMsg, ...recent] };
285
+ } catch {
286
+ runtime.diagCtxThrown++;
287
+ return; // non-fatal: no trim this call; the next context event retries
288
+ }
289
+ });
290
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * mega-events/register.ts — top-level event handler registration.
3
+ *
4
+ * Exports `lastRuntime` (DIAG accessor for the test harness) and
5
+ * `registerEventHandlers` which delegates to the focused sub-registration
6
+ * functions in session/agent/context/compact handlers.
7
+ */
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ import type { MegaRuntime } from "../mega-runtime.js";
10
+ import type { MegaConfig } from "../mega-config.js";
11
+ import { registerSessionHandlers } from "./session-handlers.js";
12
+ import { registerAgentHandlers } from "./agent-handlers.js";
13
+ import { registerContextHandler } from "./context-handler.js";
14
+ import { registerCompactHandlers } from "./compact-handlers.js";
15
+
16
+ /**
17
+ * DIAG accessor for the headless test harness: the most recently constructed
18
+ * MegaRuntime, so a test that loads the compiled extension via its default
19
+ * export can read diag counters (diagLiveTrimFires / diagBeforeCompactFires /
20
+ * diagBeforeCompactSupplied / diagAgentEndIdle) after firing synthetic events.
21
+ * No-op in production — nothing reads this outside tests.
22
+ */
23
+ export let lastRuntime: MegaRuntime | undefined;
24
+
25
+ /** Register all pi lifecycle event handlers. */
26
+ export function registerEventHandlers(
27
+ pi: ExtensionAPI,
28
+ runtime: MegaRuntime,
29
+ config: MegaConfig,
30
+ ): void {
31
+ lastRuntime = runtime;
32
+ // ---- Session lifecycle (state reset points) -------------------------------
33
+ registerSessionHandlers(pi, runtime, config);
34
+ registerAgentHandlers(pi, runtime, config);
35
+ registerContextHandler(pi, runtime, config);
36
+ registerCompactHandlers(pi, runtime, config);
37
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * mega-events/session-handlers.ts — pi session lifecycle event handlers.
3
+ *
4
+ * Registers model/provider capture, session start/tree/shutdown, and the
5
+ * before_agent_start auto-inline injection point.
6
+ */
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { normalizeSessionId } from "../../src/store.js";
9
+ import { autoMaintain } from "../../src/store/sqlite.js";
10
+ import {
11
+ type MegaRuntime,
12
+ recentUserQuery,
13
+ WIDGET_KEY,
14
+ } from "../mega-runtime.js";
15
+ import {
16
+ doRecall,
17
+ doRecallAsync,
18
+ } from "../mega-pipeline.js";
19
+ import { recallMemoriesAndInline } from "../../src/recall.js";
20
+ import type { MegaConfig } from "../mega-config.js";
21
+
22
+ /** Register session lifecycle event handlers. */
23
+ export function registerSessionHandlers(
24
+ pi: ExtensionAPI,
25
+ runtime: MegaRuntime,
26
+ config: MegaConfig,
27
+ ): void {
28
+ // Capture model/provider whenever it changes (drives real cost estimation).
29
+ pi.on("model_select", async (_event, ctx) => {
30
+ runtime.captureModel(ctx);
31
+ runtime.snapshot(ctx);
32
+ });
33
+
34
+ pi.on("session_start", async (event, ctx) => {
35
+ runtime.resetRuntime(ctx.sessionManager.getSessionId());
36
+ runtime.captureModel(ctx); // best-effort: ctx.model may be set by session start
37
+ runtime.setStatus(
38
+ ctx,
39
+ config.auto ? "mega-compact: ready" : "mega-compact: manual only",
40
+ );
41
+ // S21: clear any stale memory block from a prior session.
42
+ runtime.pendingMemoryRecallBlock = undefined;
43
+ // Auto-inline on resume/fork/continue: stage the most relevant checkpoints
44
+ // so the next before_agent_start prepends them to the system prompt.
45
+ // Triggered whenever this session already has persisted checkpoints AND a
46
+ // usable query — that covers reason "resume"/"fork" (explicit) and
47
+ // reason "startup" (e.g. `pi --continue`s an existing session, which still
48
+ // emits "startup" but with a populated message window). A brand-new empty
49
+ // session has no checkpoints, so it's naturally excluded.
50
+ if (config.autoInline) {
51
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
52
+ const query = recentUserQuery(ctx);
53
+ if (query && runtime.store.stats(sid).checkpointCount > 0) {
54
+ // S17: use the async variant on resume so cross-repo HNSW recall can
55
+ // augment when this repo's store is thin. session_start is an async-safe
56
+ // point (unlike the mid-turn context handler, which stays sync).
57
+ const r = await doRecallAsync(runtime, config, ctx, query, "resume", {
58
+ crossRepo: config.crossRepoEnabled,
59
+ });
60
+ if (!r.empty) {
61
+ runtime.pendingRecallBlock = r.block;
62
+ const crossLabel = r.toInject.some((h) => h.repoId)
63
+ ? " (cross-repo)"
64
+ : "";
65
+ runtime.setStatus(
66
+ ctx,
67
+ `mega-compact: recalled ${r.toInject.length} chkpt${crossLabel}`,
68
+ );
69
+ runtime.logger.info("auto-inline", {
70
+ reason: event.reason,
71
+ query,
72
+ injected: r.toInject.map((h) => h.checkpoint.checkpointId),
73
+ crossRepo: r.toInject.some((h) => h.repoId),
74
+ });
75
+ }
76
+ }
77
+ // S21: parallel memory recall. Same async context so we can await without
78
+ // breaking the handler contract. Best-effort — never throws.
79
+ try {
80
+ const mr = await recallMemoriesAndInline({
81
+ query,
82
+ stateDir: runtime.getStateDir(),
83
+ limit: 5,
84
+ crossRepo: config.crossRepoEnabled,
85
+ crossRepoCosine: config.crossRepoCosine,
86
+ });
87
+ if (!mr.empty) runtime.pendingMemoryRecallBlock = mr.block;
88
+ } catch (err) {
89
+ runtime.logger.warn("memory-recall skipped", { err: String(err) });
90
+ }
91
+ }
92
+ // S27 Task 10: best-effort auto-maintenance on session start (prune rows
93
+ // older than 30d, checkpoint WAL if >10MB, VACUUM if DB >100MB + >20%
94
+ // freelist). Never blocks session start — swallows errors and logs a
95
+ // one-line summary for diagnostics.
96
+ try {
97
+ const m = autoMaintain(runtime.currentStateDir);
98
+ if (m && !m.endsWith("nothing to do")) runtime.logger.info("db-auto-maintain", { result: m });
99
+ } catch (e) {
100
+ runtime.logger.warn("db-auto-maintain-fail", { error: String(e) });
101
+ }
102
+ runtime.dashboard.event("session_start", {
103
+ reason: event.reason,
104
+ sessionId: runtime.rt.sessionId,
105
+ });
106
+ runtime.snapshot(ctx);
107
+ });
108
+
109
+ pi.on("session_tree", async (_event, ctx) => {
110
+ // Branch navigation invalidates region indexes — reset checkpoint memory but
111
+ // keep the on-disk store (markers replayed from entries below if needed).
112
+ runtime.resetRuntime(ctx.sessionManager.getSessionId());
113
+ runtime.setStatus(ctx, "mega-compact: ready (branch)");
114
+ if (config.autoInline) {
115
+ const query = recentUserQuery(ctx);
116
+ if (query) {
117
+ const r = doRecall(runtime, config, ctx, query, "resume");
118
+ if (!r.empty) {
119
+ runtime.pendingRecallBlock = r.block;
120
+ runtime.logger.info("auto-inline", {
121
+ reason: "session_tree",
122
+ query,
123
+ injected: r.toInject.map((h) => h.checkpoint.checkpointId),
124
+ });
125
+ }
126
+ // S21: parallel memory recall. Trigram embedder is sub-ms; await is fine.
127
+ try {
128
+ const mr = await recallMemoriesAndInline({
129
+ query,
130
+ stateDir: runtime.getStateDir(),
131
+ limit: 5,
132
+ crossRepo: config.crossRepoEnabled,
133
+ crossRepoCosine: config.crossRepoCosine,
134
+ });
135
+ if (!mr.empty) runtime.pendingMemoryRecallBlock = mr.block;
136
+ } catch (err) {
137
+ runtime.logger.warn("memory-recall skipped", { err: String(err) });
138
+ }
139
+ }
140
+ }
141
+ runtime.dashboard.event("session_tree", {
142
+ sessionId: runtime.rt.sessionId,
143
+ });
144
+ runtime.snapshot(ctx);
145
+ });
146
+
147
+ // ---- Auto-inline injection point: prepend staged recall to systemPrompt ----
148
+ pi.on("before_agent_start", async (event, ctx) => {
149
+ runtime.captureModel(ctx); // most reliable point ctx.model is populated
150
+ const cpBlock = runtime.pendingRecallBlock;
151
+ const memBlock = runtime.pendingMemoryRecallBlock;
152
+ if (!cpBlock && !memBlock) return;
153
+ runtime.pendingRecallBlock = undefined;
154
+ runtime.pendingMemoryRecallBlock = undefined;
155
+ const composed = [cpBlock, memBlock].filter(Boolean).join("\n\n");
156
+ return { systemPrompt: `${event.systemPrompt}\n\n${composed}` };
157
+ });
158
+
159
+ pi.on("session_shutdown", async (_event, ctx) => {
160
+ runtime.setStatus(ctx, undefined);
161
+ runtime.activeAgents = 0;
162
+ runtime.currentTurn = 0;
163
+ ctx.ui.setWidget(WIDGET_KEY, [], { placement: "aboveEditor" });
164
+ });
165
+ }