import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { debugLog } from "../debug-log.js"; import { computeSessionSettings, type Runtime } from "../runtime.js"; import { launchCompactionObserver, type ConsolidationCtx } from "./consolidation-trigger.js"; import { buildCompactionProjection, renderSummary, type Entry } from "../session-ledger/index.js"; import { watchForNativeCompactionResume } from "./compaction-resume.js"; const COMPACTION_STATUS_KEY = "observational-memory-compaction"; export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void { pi.on("session_before_compact", async (event: any, ctx: any) => { if (runtime.compactHookInFlight) { if (ctx.hasUI) { ctx.ui.notify( "pi-contemplator: another compaction is already in progress; cancelling duplicate", "warning", ); } return { cancel: true }; } const initiatedByOm = runtime.compactInFlight && event.reason === "manual"; const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason; const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop"); if (ctx.hasUI) { let pending = ""; if (event.willRetry) pending = ", retry pending"; else if (omWillResume) pending = ", resume pending"; ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${reason}${pending})`); if (!initiatedByOm) { const continuation = event.willRetry ? "; the interrupted agent run will resume automatically" : ""; ctx.ui.notify(`pi-contemplator: compaction started (${reason})${continuation}`, "info"); } } event.signal?.addEventListener?.("abort", () => { if (ctx.hasUI) ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined); }, { once: true }); runtime.compactHookInFlight = true; try { runtime.ensureConfig(ctx.cwd); const { preparation, branchEntries } = event; const branch = branchEntries as Entry[]; // Start memory capture without delaying compaction. This is configurable so // users can compare native compaction with and without the observer sidecar. if (runtime.config.compactionObserverEnabled !== false) { launchCompactionObserver(pi, runtime, ctx as ConsolidationCtx, branch); } const { firstKeptEntryId, tokensBefore } = preparation; const projection = buildCompactionProjection(branch, firstKeptEntryId); const summary = renderSummary(projection.summaries, projection.observations); // Compaction removes older custom entries from the active branch. Keep // session-scoped overrides in the compaction details so they can be // restored after a reload from the surviving branch. Bake the merged // branch intent (live om.settings entries winning over earlier snapshots) // rather than the raw in-memory overlay, which can lag out-of-band // om.settings appends and would otherwise silently override newer entries // at the next restore. const details = { ...projection.details, sessionSettings: computeSessionSettings(branch), }; return { compaction: { summary, firstKeptEntryId, tokensBefore, details, }, }; } finally { runtime.compactHookInFlight = false; } }); pi.on("session_compact", (event: any, ctx: any) => { const initiatedByOm = runtime.compactInFlight && event.reason === "manual"; const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason; const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop"); if (event.willRetry) watchForNativeCompactionResume(pi, runtime, ctx); if (!ctx.hasUI) return; ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined); let continuation = ""; if (event.willRetry) continuation = "; resuming the interrupted agent run"; else if (omWillResume) continuation = "; resuming the agent run"; ctx.ui.notify(`pi-contemplator: compaction complete (${reason})${continuation}`, "info"); }); pi.on("session_compact_failed", (event, ctx) => { const initiatedByOm = runtime.compactInFlight && event.reason === "manual"; const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason; debugLog("compaction.failed", { reason, piReason: event.reason, errorMessage: event.errorMessage, aborted: event.aborted, willRetry: event.willRetry, fromExtension: event.fromExtension, initiatedByOm, }); // OM-initiated ctx.compact() calls already have an onError callback that // clears UI state and applies the origin-specific continuation policy. Do // not duplicate that work here; Pi emits this event before invoking it. if (initiatedByOm) return; if (ctx.hasUI) ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined); if (!event.aborted && ctx.hasUI) { ctx.ui.notify( `pi-contemplator: compaction failed (${reason}): ${event.errorMessage ?? "unknown error"}`, "error", ); } }); }