import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { CONTINUATION_PROMPT, sendContinuationPrompt } from "./continuation.ts"; import { EXTENSION_ID, type CompactionSource, type MidrunCompactConfig, type RuntimeState, type TriggerSnapshot, } from "./types.ts"; export interface StartCompactionOptions { source: CompactionSource; config: MidrunCompactConfig; customInstructions?: string; trigger?: TriggerSnapshot; failureKey?: string; continuationPrompt?: string; } export function createRuntimeState(): RuntimeState { return { phase: "idle", generation: 0, }; } export function resetRuntime(runtime: RuntimeState): void { runtime.generation += 1; runtime.phase = "idle"; runtime.triggerSessionId = undefined; runtime.triggerTokens = undefined; runtime.triggerPercent = undefined; runtime.triggerContextWindow = undefined; runtime.triggerSource = undefined; runtime.lastFailureKey = undefined; runtime.lastBlockedFailureKey = undefined; runtime.lastError = undefined; } export function invalidateRuntime(runtime: RuntimeState): void { resetRuntime(runtime); } export function clearRuntimeFailure(runtime: RuntimeState): void { runtime.lastFailureKey = undefined; runtime.lastBlockedFailureKey = undefined; runtime.lastError = undefined; if (runtime.phase === "failed") runtime.phase = "idle"; } export function isCompactionBusy(runtime: RuntimeState): boolean { return runtime.phase === "compacting" || runtime.phase === "resume-pending"; } export function safeSessionId(ctx: ExtensionContext): string | undefined { try { return ctx.sessionManager.getSessionId(); } catch { return undefined; } } function sameSession(ctx: ExtensionContext, expectedSessionId: string): boolean { return safeSessionId(ctx) === expectedSessionId; } function notify( ctx: ExtensionContext, config: MidrunCompactConfig, message: string, type: "info" | "warning" | "error", force = false, ): void { if (!ctx.hasUI || (!config.notify && !force)) return; try { ctx.ui.notify(`${EXTENSION_ID}: ${message}`, type); } catch { // Notifications must never break compaction control flow. } } function setStatus(ctx: ExtensionContext, config: MidrunCompactConfig, text: string | undefined): void { if (!ctx.hasUI || !config.notify) return; try { ctx.ui.setStatus(EXTENSION_ID, text); } catch { // Status UI is best-effort only. } } function describeTrigger(trigger: TriggerSnapshot | undefined): string { if (!trigger) return ""; return ` at ${trigger.tokens.toLocaleString()}/${trigger.contextWindow.toLocaleString()} tokens (${trigger.percent.toFixed(1)}%, threshold ${trigger.thresholdPercent}%)`; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function settleFailure( ctx: ExtensionContext, runtime: RuntimeState, options: StartCompactionOptions, message: string, ): void { runtime.phase = "failed"; runtime.lastFailureKey = options.failureKey; runtime.lastBlockedFailureKey = undefined; runtime.lastError = message; setStatus(ctx, options.config, undefined); notify( ctx, options.config, `compaction failed: ${message}. The interrupted task was not resumed. Fix the backend and run /midrun-compact retry, or inspect /midrun-compact status.`, "error", true, ); } export function blockRepeatedFailure( ctx: ExtensionContext, runtime: RuntimeState, config: MidrunCompactConfig, failureKey: string, ): boolean { if (runtime.lastFailureKey !== failureKey) return false; runtime.phase = "failed"; ctx.abort(); if (runtime.lastBlockedFailureKey !== failureKey) { runtime.lastBlockedFailureKey = failureKey; notify( ctx, config, "paused before another model request because compaction already failed at nearly the same context size. Run /midrun-compact status, then /midrun-compact retry after fixing the compaction backend.", "error", true, ); } return true; } /** * Start one Pi-owned compaction operation. ctx.compact() internally aborts the * active run before entering Pi's standard compaction pipeline. */ export function startCompaction( pi: ExtensionAPI, ctx: ExtensionContext, runtime: RuntimeState, options: StartCompactionOptions, ): boolean { if (isCompactionBusy(runtime)) { notify(ctx, options.config, "a compaction or resume dispatch is already in progress.", "warning", true); return false; } const sessionId = safeSessionId(ctx); if (!sessionId) { settleFailure(ctx, runtime, options, "unable to identify the active session"); return false; } runtime.phase = "compacting"; runtime.generation += 1; runtime.triggerSessionId = sessionId; runtime.triggerTokens = options.trigger?.tokens; runtime.triggerPercent = options.trigger?.percent; runtime.triggerContextWindow = options.trigger?.contextWindow; runtime.triggerSource = options.source; runtime.lastError = undefined; runtime.lastBlockedFailureKey = undefined; const generation = runtime.generation; setStatus(ctx, options.config, "compacting"); notify( ctx, options.config, `${options.source === "threshold" ? "context threshold reached" : `${options.source} requested`}; starting Pi compaction${describeTrigger(options.trigger)}.`, "info", ); let callbackSettled = false; const claimCallback = (): boolean => { if (callbackSettled) return false; callbackSettled = true; return runtime.generation === generation && runtime.phase === "compacting" && sameSession(ctx, sessionId); }; try { ctx.compact({ customInstructions: options.customInstructions, onComplete: () => { if (!claimCallback()) return; runtime.lastFailureKey = undefined; runtime.lastBlockedFailureKey = undefined; runtime.lastError = undefined; if (!options.config.autoResume) { runtime.phase = "idle"; setStatus(ctx, options.config, undefined); notify(ctx, options.config, "compaction completed; automatic resume is disabled.", "info"); return; } runtime.phase = "resume-pending"; setStatus(ctx, options.config, "resume pending"); // Pi flushes input queued during compaction from its compaction-end // handler. Wait until the next event-loop turn so isIdle() reflects // whether that input, an automatic retry, or another extension has // already started the continuation. setImmediate(() => { if ( runtime.generation !== generation || runtime.phase !== "resume-pending" || !sameSession(ctx, sessionId) ) { return; } let idle: boolean; try { idle = ctx.isIdle(); } catch (error) { settleFailure(ctx, runtime, options, `unable to inspect continuation state: ${errorMessage(error)}`); return; } runtime.phase = "idle"; setStatus(ctx, options.config, undefined); if (!idle) { notify( ctx, options.config, "compaction completed; an existing turn or queued message will continue the session, so no extra continuation was sent.", "info", ); return; } try { sendContinuationPrompt(pi, options.continuationPrompt ?? CONTINUATION_PROMPT); notify(ctx, options.config, "compaction completed; continuation sent.", "info"); } catch (error) { settleFailure(ctx, runtime, options, `unable to dispatch continuation: ${errorMessage(error)}`); } }); }, onError: (error) => { if (!claimCallback()) return; settleFailure(ctx, runtime, options, errorMessage(error)); }, }); } catch (error) { if (!callbackSettled) callbackSettled = true; if (runtime.generation === generation && sameSession(ctx, sessionId)) { settleFailure(ctx, runtime, options, errorMessage(error)); } return false; } return true; }